Back-End Spring Boot REST API for the Spawn Mobile App, with a MySQL database, which we interface with through JPA (Java Persistence API).
Spring Boot is our back-end framework with the Java language. It handles the API requests and responses, between our controller, service, and repository layers (see above in here).
- Spring Annotations
@RestController
tells Spring that this class is a controller, and that it should handle incoming HTTP requests (GET, POST, PUT, DELETE, etc.)- We have various mappings for these types of requests, like
@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
, etc.- This is also where we specify the path of these requests, along with:
@PathVariable
for URL parameters@RequestParam
for query parameters@RequestBody
for the request body (in a POST request, for example)
- This is also where we specify the path of these requests, along with:
- We have various mappings for these types of requests, like
@Service
tells Spring that this class is a service, and that it should be managed by Spring@Autowired
tells Spring to inject the dependency (e.g.EventService
intoEventController
)@Repository
tells Spring that this class is a repository
- Beans
- Beans refer to the instantiations of our classes, which get managed by Spring. For example, our service classes, since they are concrete implementations of our interfaces (e.g.
UserService
, being the implementation ofIUserService
), are beans. - So, we don't have to manage circular dependencies between classes. For example, since
UserService
takes inEventService
as a dependency, and vice versa,EventService
takes inUserService
, we can annotate them with@Autowired
to let Spring handle that issue
- Beans refer to the instantiations of our classes, which get managed by Spring. For example, our service classes, since they are concrete implementations of our interfaces (e.g.
@Entity
tells JPA that this class is an entity, and that it should be mapped to a table in the database@Id
tells JPA that this field is the primary key of the table@GeneratedValue
tells JPA that this field is auto-generated- There are also other strategies for generating primary keys, like
GenerationType.IDENTITY
,GenerationType.SEQUENCE
, etc. - For ids of a table, there are also
@EmbeddedId
for composite primary keys, for example in theEventParticipants
table
@Column
is used to specify the column name, length, nullable, etc.@OneToMany
and@ManyToOne
are used to specify the relationships between entitiesJpaRepository
is an interface that extendsCrudRepository
, which provides CRUD operations for the entity- So, there are pre-defined generic methods like
save
,findById
,findAll
,delete
, etc. - We can also define custom queries in the repository interface, by using the
@Query
annotation- An example of a custom query is in the
UserRepository
interface, where we find users by their username - Also, with custom queries, they can be generated for us by simply naming the method according to JPA standards, like in the
EventRepository
interface, where we find events by their start time using the method,List<Event> findByCreatorId(UUID creatorId);
- An example of a custom query is in the
- So, there are pre-defined generic methods like
The app implements client-side caching to improve performance and reduce API calls. The caching system includes:
- AppCache Singleton: A centralized cache store that persists data to disk and provides reactive updates
- Cache Validation API: A backend API endpoint that validates cached data and informs the client when to refresh
- Push Notification Support: Real-time updates when data changes server-side
The Spawn App iOS client implements a sophisticated caching mechanism to reduce API calls, speed up the app's responsiveness, and provide a better user experience. This is achieved through:
- Client-side caching: Storing frequently accessed data locally
- Cache invalidation: Checking with the backend to determine if cached data is stale
- Push notifications: Receiving real-time updates when relevant data changes
The app caches several types of data to enhance performance:
- Friends List: A user's complete friends list
- Events: Events the user created or was invited to
- Profile Pictures: Both the user's own profile picture and those of friends
- Recommended Friends: Potential friends recommended by the system
- Friend Requests: Pending friend requests
- Tags: User-created friend tags
- Tagged Friends: Friends categorized in each tag
- User Blocking: When a user blocks another user, relevant caches are invalidated to ensure they don't appear in each other's recommended friends lists, friend lists, or other profile views
- Event Creation: New events trigger cache invalidation for invited users
The app makes a request to /api/v1/cache/validate/:userId
on startup, sending a list of cached items and their timestamps:
{
"friends": "2025-04-01T10:00:00Z",
"events": "2025-04-01T10:10:00Z",
"profilePicture": "2025-04-01T10:15:00Z",
"otherProfiles": "2025-04-01T10:20:00Z",
"recommendedFriends": "2025-04-01T10:25:00Z",
"friendRequests": "2025-04-01T10:30:00Z",
"userTags": "2025-04-01T10:35:00Z",
"tagFriends": "2025-04-01T10:40:00Z"
}
The backend responds with which items need to be refreshed:
{
"friends": {
"invalidate": true,
"updatedItems": [...] // Optional
},
"events": {
"invalidate": false
},
"profilePicture": {
"invalidate": true,
"updatedItems": [...] // Optional
}
}
The app listens for push notifications with specific types that indicate data changes:
friend-accepted
: When a friend request is acceptedfriend-blocked
: When a user is blockedevent-updated
: When an event is updatedevent-created
: When a new event is createdprofile-updated
: When a user's profile is updatedtag-updated
: When a user's tags are modified
When these notifications are received, the app refreshes the relevant cached data.
- On app launch,
AppCache
loads cached data from disk - The app sends a request to validate the cache with the backend
- For invalidated cache items:
- If the backend provides updated data, it's used directly
- Otherwise, the app fetches the data with a separate API call
- As the user uses the app, they see data from the cache immediately
- In the background, the app may update cache items based on push notifications
- App loads cached data → UI renders immediately
- App checks if cache is valid → Updates UI if needed
- User interacts with fresh data → Great experience!
- Speed: UI renders instantly from cache
- Bandwidth: Reduced API calls
- Battery: Less network activity
- Offline Use: Basic functionality without network
The backend implements a cache validation endpoint at /api/v1/cache/validate/:userId
that:
- Receives a map of cache categories and their last update timestamps
- Compares these timestamps to when data was last modified on the server
- Responds with which categories need refreshing and optionally includes updated data
- Sends push notifications when relevant data changes
Note that for the first two, you'll need access to Railway, so only @ShaneMander, @evannawfal, and I (@Daggerpov) will be able to follow them. The last one (setting up local MySQL database) is for everyone.
Connecting to Prod (Hosted) DB Through IntelliJ
- Create new data source from URL
- On Railway -> go to our database's container -> "Data" tab -> click "Connect"
- Click "Public Network" -> copy the first URL
- Paste that into the IntelliJ connection window
- Click "no auth" instead of user/pass login
- Prepend
jdbc:
to the URL and click "test connection"
- It should work now, and show the DB tables within IntelliJ here:
Exporting DB Data to Excel File
One use case, that I'll use for this guide: beta access sign up emails, for our marketing team.
- From IntelliJ's Database panel/tab, expand
{your DB connection name}/railway/tables
:
- Right-click on the table -> "Import/Export" -> "Export Data to File":
- You're done with the click of this button
Setup Local MySQL Database
Follow these steps to download, set up the database locally, create spawn_db
, and populate it with sample data.
-
Download MySQL Community Server:
- Visit the MySQL Downloads Page.
- Choose your operating system and download the installer.
- Follow the installation wizard steps.
- Configure the root password during setup and remember it for later use.
-
(Optional) Download MySQL Workbench:
- Visit the MySQL Workbench Page.
- Install it to have a graphical interface to work with your MySQL server.
-
Open MySQL Workbench or Terminal:
- If using MySQL Workbench, connect to your MySQL server and log in.
- If using Terminal, log in to the MySQL server by running:
mysql -u root -p
- Enter your root password when prompted.
-
Create the
spawn_db
Database:- Run the following command to create the database:
CREATE DATABASE spawn_db;
- Run the following command to create the database:
-
Use the
spawn_db
Database:- Run the following command:
USE spawn_db;
- Run the following command:
-
Ensure Environment Variables are Set
- Set the following environment variables, or add to a
.env
fileMYSQL_URL
MYSQL_USER
MYSQL_PASSWORD
- Set the following environment variables, or add to a
-
Populate the Database with Sample Data:
- If you are using the terminal, create a file named
populate_spawn_db.sql
and add the following SQL commands to it:-- Use the spawn_db database USE spawn_db; -- Populate Users INSERT INTO user (id, username, first_name, last_name, bio, profile_picture) VALUES (UNHEX(REPLACE(UUID(), '-', '')), 'john_doe', 'John', 'Doe', 'Loves hiking and coffee.', 'profile1.png'), (UNHEX(REPLACE(UUID(), '-', '')), 'jane_smith', 'Jane', 'Smith', 'Digital nomad and bookworm.', 'profile2.png'), (UNHEX(REPLACE(UUID(), '-', '')), 'sam_wilson', 'Sam', 'Wilson', 'Coder by day, gamer by night.', 'profile3.png'), (UNHEX(REPLACE(UUID(), '-', '')), 'alex_jones', 'Alex', 'Jones', 'Photographer with a passion for travel.', 'profile4.png'); -- Populate Locations INSERT INTO location (id, name, latitude, longitude) VALUES (UNHEX(REPLACE(UUID(), '-', '')), 'Central Park', 40.785091, -73.968285), (UNHEX(REPLACE(UUID(), '-', '')), 'Times Square', 40.758896, -73.985130), (UNHEX(REPLACE(UUID(), '-', '')), 'Golden Gate Park', 37.769042, -122.483519), (UNHEX(REPLACE(UUID(), '-', '')), 'Eiffel Tower', 48.858844, 2.294351); -- Populate Events INSERT INTO event (id, title, start_time, end_time, location_id, note, creator_id) VALUES (UNHEX(REPLACE(UUID(), '-', '')), 'Hiking Adventure', '2024-12-01T08:00:00', '2024-12-01T16:00:00', (SELECT id FROM location WHERE name='Central Park'), 'Bring snacks and water.', (SELECT id FROM user WHERE username='john_doe')), (UNHEX(REPLACE(UUID(), '-', '')), 'Book Club Meeting', '2024-12-05T18:00:00', '2024-12-05T20:00:00', (SELECT id FROM location WHERE name='Times Square'), 'Discussing the latest thriller.', (SELECT id FROM user WHERE username='jane_smith')), (UNHEX(REPLACE(UUID(), '-', '')), 'Photography Workshop', '2024-12-10T10:00:00', '2024-12-10T15:00:00', (SELECT id FROM location WHERE name='Golden Gate Park'), 'Learn the basics of DSLR photography.', (SELECT id FROM user WHERE username='alex_jones')); -- Populate Event Participants INSERT INTO event_participants (event_id, user_id) VALUES ((SELECT id FROM event WHERE title='Hiking Adventure'), (SELECT id FROM user WHERE username='jane_smith')), ((SELECT id FROM event WHERE title='Hiking Adventure'), (SELECT id FROM user WHERE username='sam_wilson')), ((SELECT id FROM event WHERE title='Book Club Meeting'), (SELECT id FROM user WHERE username='john_doe')), ((SELECT id FROM event WHERE title='Book Club Meeting'), (SELECT id FROM user WHERE username='alex_jones')); -- Populate Event Invited INSERT INTO event_invited (event_id, user_id) VALUES ((SELECT id FROM event WHERE title='Photography Workshop'), (SELECT id FROM user WHERE username='john_doe')), ((SELECT id FROM event WHERE title='Photography Workshop'), (SELECT id FROM user WHERE username='jane_smith')); -- Populate Friend Tags INSERT INTO friend_tag (id, display_name, color) VALUES (UNHEX(REPLACE(UUID(), '-', '')), 'Close Friends', '#FF5733'), (UNHEX(REPLACE(UUID(), '-', '')), 'Work Friends', '#33FF57'), (UNHEX(REPLACE(UUID(), '-', '')), 'Family', '#3357FF'); -- Populate Friend Requests INSERT INTO friend_requests (id, sender_id, receiver_id) VALUES (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='john_doe'), (SELECT id FROM user WHERE username='jane_smith')), (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='sam_wilson'), (SELECT id FROM user WHERE username='alex_jones')); -- Populate User Friends INSERT INTO user_friends (id, friend_1, friend_2) VALUES (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='john_doe'), (SELECT id FROM user WHERE username='sam_wilson')), (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='jane_smith'), (SELECT id FROM user WHERE username='alex_jones')); -- Populate User Friend Tags INSERT INTO user_friend_tags (id, user_id, friend_tag_id) VALUES (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='john_doe'), (SELECT id FROM friend_tag WHERE display_name='Close Friends')), (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='jane_smith'), (SELECT id FROM friend_tag WHERE display_name='Work Friends')); -- Populate User Friend Tag Mapping INSERT INTO user_friend_tag_mapping (id, user_1, user_2, friend_tag_id) VALUES (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='john_doe'), (SELECT id FROM user WHERE username='sam_wilson'), (SELECT id FROM friend_tag WHERE display_name='Close Friends')), (UNHEX(REPLACE(UUID(), '-', '')), (SELECT id FROM user WHERE username='jane_smith'), (SELECT id FROM user WHERE username='alex_jones'), (SELECT id FROM friend_tag WHERE display_name='Work Friends'));
- If you are using the terminal, create a file named
-
Run the Script:
- In the terminal, run:
mysql -u root -p spawn_db < populate_spawn_db.sql
- If using MySQL Workbench, paste the script into the query window and run it.
- In the terminal, run:
Thats all! You have successfully set up the spawn_db
database locally and populated it with sample data. You can now
use it to test the Spawn application. (hopefully)
Admin Dashboard Login
The admin dashboard requires authentication to access protected endpoints. When you start the application for the first time, an admin user is automatically created with credentials from environment variables.
Set the following environment variables in your Railway project or local environment:
ADMIN_USERNAME
- Username for the admin account (defaults to "admin" if not set)ADMIN_PASSWORD
- Password for the admin account (defaults to a secure password if not set)
These environment variables should be added to your Railway project settings in the same way other sensitive information is stored.
- Navigate to the admin login page at
/admin
- Enter the admin credentials configured via environment variables
- Upon successful authentication, you will be redirected to the
/admin/dashboard
page - JWT tokens are stored in the browser's local storage and automatically used for API requests
- The application handles token expiration and refresh automatically
- The password is stored using BCrypt encryption in the database
- JWT tokens have a default expiry time (24 hours for access tokens, 180 days for refresh tokens)
- Credentials are configured via environment variables rather than hardcoded values
For additional security in production environments, consider:
- Using a complex, randomly generated password
- Implementing rate limiting for login attempts
- Using HTTPS to secure token transmission