System Design
System Architecture
Below is the system architecture diagram for our Community Impact Portal web application. The diagram illustrates how the frontend and backend components interact using React, axios for communication, and Django (with Django REST Framework and Django Models) for processing and data storage.
How the Architecture Works
The front-end is built with React. The Router manages client-side navigation, allowing users to switch between views without reloading the entire page. The Components are individual UI elements that display data and handle user interactions. When data is needed, a page/component uses axios to send HTTP requests and receive responses from the back-end.
On the back-end, Django is used alongside the Django REST Framework (DRF). The REST Framework receives HTTP requests from the front-end, applies authentication, validation, and data serialisation/deserialisation, and then returns a response in JSON format. The Model in Django represents the data structure and business logic of the application. It interacts with the database using Django’s ORM.
The Database is a persistent storage layer that stores user data, posts, or any other domain-specific records. The Model sends SQL queries to the Database, which returns the corresponding query results. These results are then passed back through the REST Framework to form the API responses.
This architecture separates the front-end and back-end responsibilities: React manages the user interface and routing, axios handles HTTP communications, the Django REST Framework serves as the API layer and Django Models encapsulate the data logic and interact with the Database.
Design Principles and Patterns
Design Patterns
Singleton Design Pattern [1]
Company Information:
The CompanyInformation
model demonstrates the application of the Singleton design pattern. By overriding the save
method, we enforce that only one instance of this model can exist within the database. This ensures consistent, centralized storage of company-specific details, preventing data conflicts and simplifying access to crucial information like logo, branding colors, and map boundaries. This pattern is particularly useful for managing global application settings or configurations where multiple instances would lead to inconsistencies. Furthermore, the model's design adheres to the principle of single responsibility, focusing solely on storing and managing company-related data, thus promoting cleaner and more maintainable code.
...
from django.db import models
class CompanyInformation(models.Model):
...
def save(self, *args, **kwargs):
"""
Ensure there is only one instance of this model in the database.
"""
if not self.pk and CompanyInformation.objects.exists():
raise ValueError("Only one instance of CompanyInformation can exist.")
super().save(*args, **kwargs)
class CompanyInformationViewSet(viewsets.ModelViewSet):
...
def create_default_company():
"""
Creates a default company if none exists in the database.
This function checks if any CompanyInformation instances exist and, if not,
creates a default company with predefined attributes such as name, about,
color, font, and geographic boundaries.
"""
try:
if not CompanyInformation.objects.exists():
# Create an example company if it doesn't exist
CompanyInformation.objects.create(
name="Example Company",
about="This is an example company. It's just a placeholder.",
logo=None, # You can leave it as None or add a default image if available
main_color="#FF5733", # Hex color code example
font="Arial", # Default font
sw_lat = 51.341875, # Example latitude for SW corner
sw_lon = -0.29222, # Example longitude for SW corner
ne_lat = 51.651675, # Example latitude for NE corner
ne_lon = 0.01758, # Example longitude for NE corner
)
except OperationalError:
# The table doesn't exist yet, skip creating the default company
pass
# Call the function to create the default company when the application starts
create_default_company()
AI Context
We also employ this design pattern with AIContext and AIProvider. Only one large language model (LLM) engine is instantiated at one time. All other components use AIContext as a single shared resource, as it is passed in as a context. By using this Singleton pattern, the LLM engine is centrally managed, and all components can leverage it as a single resource without unnecessary duplication or overhead.
...
import { CreateWebWorkerMLCEngine } from "@mlc-ai/web-llm";
// Create a context
export const AIContext = createContext();
...
const initModel = async () => {
try {
const createdEngine = await CreateWebWorkerMLCEngine(
new Worker(new URL(".././workers/worker.jsx", import.meta.url), {
type: "module",
}),
modelToUse,
{ initProgressCallback }
);
setEngine(createdEngine);
} catch (error) {
console.error("Error while loading model:", error);
}
};
...
return (
<AIContext.Provider
value={{ getReply, engine, progressModelLoaded, modelDisabled }}
>
{children}
</AIContext.Provider>
);
};
import { AIContext } from "../../context/AIContext";
...
const { engine } = useContext(AIContext);
...
try {
await engine.resetChat();
const messages = [
{
role: "system",
content:
"Suggest an alternative title that is more appealing for the following title: , dont add any commentary , just generate one title maximum",
},
{
role: "user",
content: title,
},
];
...
Strategy Design Pattern [2] + Keep It Simple Stupid (KISS)
In the handleSubmit function of the Search Bar
, the process of deciding whether to perform a search or create a new report is dynamically handled based on the user's input. This approach exemplifies the Strategy Pattern, where the behavior (search or create a report) is determined by the context, i.e., the content of the user’s input. Specifically, the determineUserQuery function is responsible for evaluating the user's query and categorizing it as either a search request or a report creation request.
Additionally, the KISS principle is applied here by allowing the language model (LLM) to automatically determine the appropriate action and return a 0 or a 1. The decision-making process is simple and clear, avoiding unnecessary complexity.
...
const determineUserQuery = async (userQuery) => {
if (userQuery === "" || isStreaming) {
return;
}
const systemPrompt = `
Determine whether the following is a new report request or a search query.
- Respond with 1 (new report request) if the input:
* Asks to create a new report.
* Describes an issue or problem that needs to be reported.
* Uses phrases like "report," "create a report," or "make a new report."
- Respond with 0 (search query) if the input:
* Asks for information or looks up existing data.
* Uses phrases like "find," "search," or "information about."
Examples:
- "Can you make a new report? I noticed some overflowing bins on my road." → 1
- "Find information about overflowing bins in my area." → 0
- "I want to report a pothole on Main Street." → 1
- "What are the rules for waste disposal?" → 0
Respond with 1 for new report requests and 0 for search queries.
`;
const modelReply = await getReply(userQuery, systemPrompt, () => { }, setIsStreaming);
const handleSubmit = async (e) => {
e.preventDefault();
// LLM decides whether to initiate new report or search query
setMessages([...messages, { text: userQuery, sender: "user" }]);
...
const choice = await determineUserQuery(userQuery);
...
switch (choice) {
case "0":
getSearchReply(userQuery);
break;
case "1":
getReportReply(userQuery);
break;
default:
getSearchReply(userQuery);
break;
}
Observer Design Pattern [3]
In our project we've used the observer design pattern throughout our component code. Higher-level components act as parents to automatically update child components' data. For example, ReportsSection.jsx
changes and notifies of state changes of selectedMarker
, newMarker
and reports
, propagating those changes to SidebarReport.jsx
and MapComponent.jsx
. This observer pattern has helped make UI updates synchronous across components.
...
const ReportsSection = ({userQuery}) => {
...
const [selectedMarker, setSelectedMarker] = useState(location.state?.selectedIssue || null);
const [newMarker, setNewMarker] = useState(null);
const [reports, setReports] = useState([]);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
...
<div..>
...
<SidebarReport
selectedMarker={selectedMarker}
fetchReports={fetchReports}
></SidebarReport>
</div>
...
<div...>
<MapComponent
onMarkerSelected={handleMarkerSelected}
reports={reports}
activeFilters={getActiveFilters()}
selectedMarker={selectedMarker}
mapRef={mapRef}
userQuery={userQuery}
></MapComponent>
...
The observer pattern is also used with our context components like CompanyContext
, which shares its information about the company, such as its main_color
through the useContext
hook. This approach is an efficient way to manage shared state in React and ensures that components remain in sync with the central data source without needing to manually propagate changes.
...
// Create a context
export const CompanyContext = createContext();
// Create a provider component
export const CompanyProvider = ({ children }) => {
const [companyInfo, setCompanyInfo] = useState({
id: 1, // Default ID
name: "Example Company", // Default name
about: "This is an example company. It's just a placeholder.", // Default about
logo: null, // Default logo (null if not provided)
main_color: "#000000", // Default color
font: "Arial", // Default font
sw_lat: "34.052235", // Default SW latitude
sw_lon: "-118.243683", // Default SW longitude
ne_lat: "34.052255", // Default NE latitude
ne_lon: "-118.243600", // Default NE longitude
});
...
return (
<CompanyContext.Provider value={companyInfo}>
{children}
</CompanyContext.Provider>
);
...
...
import { CompanyContext } from "../../context/CompanyContext";
...
const Calendar = () => {
...
const { main_color } = useContext(CompanyContext);
...
<button
className="px-4 py-2 rounded-lg font-bold text-black hover:scale-110 transition active:duration-100 hover:duration-500 duration-500"
style={{
color: main_color,
outline: "solid " + main_color,
backgroundColor: "white", // Default background color
}}
...
Design Principles
Single Responsibility Principles [4]
The AuthProvider
component is solely responsible for managing all authentication-related functionality on the frontend. By abstracting this logic into one component for one purpose (AuthProvider
), we follow the Single Responsibility Principles. Other components, like Header.jsx
, can easily query the results of this component and make decisions on whether to display UI elements such as the "Manage" tab. This separation of concerns allows our components to be more lightweight and easier to maintain.
...
const AuthContext = createContext();
...
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({
isAuthenticated: false,
token: null,
user: null, //
});
...
// Login function - stores token and fetches user data
const login = async (token) => {
localStorage.setItem('token', token);
try {
const response = await axios.get(`${API_URL}api/auth/user/`, {
headers: { Authorization: `Bearer ${token}` }
});
console.log("User Data After Login:", response.data);
setAuth({
isAuthenticated: true,
token,
user: response.data, //
});
} catch (error) {
console.error("Failed to fetch user data", error);
}
};
...
return (
<AuthContext.Provider value={{ auth, login, logout }}>
{children}
</AuthContext.Provider>
);
...
...
import { useAuth } from "../context/AuthContext";
...
const Header = () => {
const { auth, logout } = useAuth();
...
{auth.user?.is_superuser && (
<div className="flex">
<Link
to="/contentmanagementsystem"
...
>
Manage
</Link>
...
Separation of Concerns Principle [5]
Due to the use of Django Rest Framework (DRF), our backend applications e.g: events
or reports
adheres to the separation of concerns principle. Each component has a specific and clear responsibility in the backend. The serializer class is tasked with converting model data to a JSON format for communication with the client, the ViewSet class is responsible for handling business logic and managing HTTP requests, the model defines the fields, behaviors of the data, and so on.
By clearly separating these concerns, the system becomes more maintainable, as changes in one part of the system (such as adding new fields to a model or modifying the business logic for a specific endpoint) won't necessarily affect other parts. It also makes the codebase more modular and easier to understand, which is especially helpful as the application grows in complexity.








Interface Segregation Principle [6]
The Interface Segregation Principle in Django promotes modularity by splitting an application into distinct, focused apps, such as reports
and comments
. Each app is responsible for handling a specific set of functionalities, streamlining both the backend and frontend experience. For example, in the comments
app, users only need to provide data relevant to comments, like content
and author
, without worrying about the details of reports. Meanwhile, the reports
app manages all report-related data.
This separation not only keeps the codebase clean but also improves API efficiency. When submitting a comment, the frontend only sends the necessary data—like content
and report_id
—which reduces the amount of unnecessary data being transmitted, leading to faster and more efficient interactions. By isolating responsibilities, Django makes it easier to maintain and scale the application, while ensuring each part is focused, streamlined, and easier to manage.
Site Map
Here is a diagram that shows the workflow of our application.
UML Diagram
Here is a diagram of our models in the Django backend. For simplicity, some information has been omitted.
Data Storage
Below is the ER diagram for our database.
References
[1] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. pp. 127ff. ISBN 0-201-63361-2.
[2] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. pp. 315ff. ISBN 0-201-63361-2.
[3] Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. pp. 293ff. ISBN 0-201-63361-2.
[4] DeMarco, Tom. (1979). Structured Analysis and System Specification. Prentice Hall. ISBN 0-13-854380-1.
[5] Laplante, Phillip (2007). What Every Engineer Should Know About Software Engineering. CRC Press. ISBN 978-0-8493-7228-5.
[6] Martin, Robert (2002). Agile Software Development: Principles, Patterns, and Practices. Pearson Education.