A robust Spring Boot application that exposes Employee and Department data via a powerful GraphQL API to handle CRUD operations for Employee and Department entities.
This project implements One-To-Many
relationships between Employee as the parent entity and the child entities DepartmentEmployee, SalaryEmployee, and TitleEmployee. The relationship is managed using Spring Data JPA with Hibernate, and the project utilizes EmbeddedId
for composite primary keys
in the relationship tables.
The following is the relationship between tables:
- Department ↔ DepartmentEmployee (
One-to-Many
) - Employee ↔ DepartmentEmployee (
One-to-Many
) - Employee ↔ SalaryEmployee (
One-to-Many
) - Employee ↔ TitleEmployee (
One-to-Many
)
These tables are managed using EmbeddedId
to define composite primary keys:
- department_employee (employee_id, department_id)
- salary (employee_id, from_date)
- title (employee_id, title, from_date)
All API access is securely protected using an API Key passed via HTTP headers. This ensures that only authorized clients can access protected resources. The API expects an X-API-KEY
header to be included in every request. If the API key is missing or invalid, the server will respond with Unauthorized
message. This mechanism secures the GraphQL endpoint (/graphql
) and any other sensitive routes by verifying the provided key against a pre-configured value stored securely in the application's configuration.
GraphQL offers several advantages over traditional REST APIs, making it a compelling choice for API development:
- Precise Data Retrieval – Clients can request exactly the data they need, reducing over-fetching and under-fetching issues common in REST. This leads to more efficient data usage and optimized network performance.
- Single Endpoint – Unlike REST, which often requires multiple endpoints for different resources, GraphQL operates through a single endpoint. This simplifies API interactions and reduces the complexity of managing numerous endpoints.
- Flexible and Strongly Typed Schema – GraphQL uses a type system to describe data, allowing clients to understand available data and operations clearly. This enhances development efficiency and reduces errors.
- Efficient Data Aggregation – Clients can fetch related data in a single request, minimizing the need for multiple roundtrips to the server. This is particularly beneficial for complex systems where data resides in different sources.
- API Evolution Without Versioning – GraphQL enables APIs to evolve by adding new fields and types without impacting existing queries. Deprecated fields can be managed gracefully, reducing the need for versioning.
These features make GraphQL a powerful alternative to REST, offering more efficient, flexible, and developer-friendly API interactions.
The technology used in this project are:
Dependency | Description |
---|---|
Spring Boot Starter Web | Provides foundational web support, enabling HTTP handling and server-side capabilities for GraphQL over HTTP. |
Spring Boot Starter GraphQL | Integrates GraphQL Java into the Spring ecosystem. |
GraphQL Java Extended Scalars | Provides additional scalar types such as DateTime , URL , and BigDecimal for GraphQL Java. |
Spring Boot Starter Validation | Adds support for Java Bean Validation via annotations like @NotNull , @Size . |
Spring Boot Starter Security | Provides components for securing the application. |
PostgreSQL | Relational database for persisting employee, department, salary, and title data. |
Hibernate | Simplifies database interactions via ORM. |
Lombok | Reduces boilerplate code through annotations. |
The project is organized into the following package structure:
📁 graphql-employee-management/
└── 📂src/
└── 📂main/
├── 📂docker/
│ ├── 📂app/ # Dockerfile for Spring Boot application (runtime container)
│ │ └── Dockerfile # Uses base image, copies JAR/dependencies, defines ENTRYPOINT
│ └── 📂postgres/ # Custom PostgreSQL Docker image (optional)
│ ├── Dockerfile # Extends from postgres:17, useful for init customization
│ └── init.sql # SQL script to create database, user, and grant permissions
├── 📂java/
│ ├── 📂config/
│ │ ├── 📂graphql/ # Configuration specific to GraphQL (e.g., scalars, schema wiring, error handling)
│ │ └── 📂security/ # Security-related configuration (e.g., API key filters, providers, SecurityFilterChain)
│ ├── 📂controller/ # REST API endpoints (e.g., EmployeeController, DepartmentController)
│ ├── 📂dto/ # Data Transfer Objects for requests/responses
│ ├── 📂entity/ # JPA entity classes mapped to database tables
│ ├── 📂mapper/ # MapStruct or manual mappers between DTO and entity
│ ├── 📂repository/ # Spring Data JPA interfaces for database access
│ └── 📂service/ # Business logic layer
│ └── 📂impl/ # Service implementation classes
└── 📂resources/
├── 📂graphql/ # GraphQL schema definitions (`.graphqls`) used by the application
├── application.properties # Application configuration (DB, profiles, etc.)
└── import.sql # SQL file for seeding database on startup
Follow these steps to set up and run the project locally:
Make sure the following tools are installed on your system:
Tool | Description | Required |
---|---|---|
Java 17+ | Java Development Kit (JDK) to run the Spring application | ✅ |
PostgreSQL | Relational database to persist application data | ✅ |
Make | Automation tool for tasks like make run-app |
✅ |
Docker | To run services like PostgreSQL in isolated containers | |
GraphQL Playground | GUI tool to interactively test GraphQL queries and mutations |
- Ensure Java 17 is installed on your system. You can verify this with:
java --version
- If Java is not installed, follow one of the methods below based on your operating system:
Using apt (Ubuntu/Debian-based):
sudo apt update
sudo apt install openjdk-17-jdk
-
Use https://adoptium.net to download and install Java 17 (Temurin distribution recommended).
-
After installation, ensure
JAVA_HOME
is set correctly and added to thePATH
. -
You can check this with:
echo $JAVA_HOME
-
Install PostgreSQL if it’s not already available on your machine:
- Use https://www.postgresql.org/download/ to download PostgreSQL.
-
Once installed, create the following databases:
CREATE DATABASE employees;
These databases are used for development and automated testing, respectively.
This project uses a Makefile
to streamline common tasks.
Install make
if not already available:
Install make
using APT
sudo apt update
sudo apt install make
You can verify installation with:
make --version
If you're using PowerShell:
- Install Chocolatey (if not installed):
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
- Verify
Chocolatey
installation:
choco --version
- Install
make
viaChocolatey
:
choco install make
After installation, restart your terminal or ensure make
is available in your PATH
.
Clone the repository:
git clone https://github.com/yoanesber/Spring-Boot-Graphql-Employee-Management.git
cd Spring-Boot-Graphql-Employee-Management
Set up your application.properties
in src/main/resources
:
# application configuration
spring.application.name=graphql-employee-management
server.port=8080
spring.profiles.active=development
## datasource configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/employees
spring.datasource.username=appuser
spring.datasource.password=app@123
spring.datasource.driver-class-name=org.postgresql.Driver
spring.sql.init.mode=always
## hibernate configuration
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=true
# API Key configuration
spring.graphql.api-key=4e1f2d3a4b5c6d7e8f9g0h1i2j3k4l5m6n7o8p9
- 🔐 Notes: Ensure that:
- Database URLs, username, and password are correct.
spring.datasource.username=appuser
,spring.datasource.password=app@123
: It's strongly recommended to create a dedicated database user instead of using the default postgres superuser.
For security reasons, it's recommended to avoid using the default postgres superuser. Use the following SQL script to create a dedicated user (appuser
) and assign permissions:
-- Create appuser and database
CREATE USER appuser WITH PASSWORD 'app@123';
-- Allow user to connect to database
GRANT CONNECT ON DATABASE employees TO appuser;
-- Grant permissions on public schema
GRANT USAGE, CREATE ON SCHEMA public TO appuser;
-- Grant all permissions on existing tables
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;
-- Grant all permissions on sequences (if using SERIAL/BIGSERIAL ids)
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO appuser;
-- Ensure future tables/sequences will be accessible too
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO appuser;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO appuser;
Update your application.properties
accordingly:
spring.datasource.username=appuser
spring.datasource.password=app@123
This section provides step-by-step instructions to run the application either locally or via Docker containers.
- Notes:
- All commands are defined in the
Makefile
. - To run using
make
, ensure thatmake
is installed on your system. - To run the application in containers, make sure
Docker
is installed and running.
- All commands are defined in the
Ensure PostgreSQL are running locally, then:
make dev
To build and run all services (PostgreSQL, Spring app):
make docker-start-all
To stop and remove all containers:
make docker-stop-all
- Notes:
- Before running the application inside Docker, make sure to update your
application.properties
- Replace
localhost
with the appropriate container name for services like PostgreSQL. - For example:
- Change
localhost:5432
tographql-postgres:5432
- Change
- Replace
- Before running the application inside Docker, make sure to update your
Now your application is accessible at:
http://localhost:8080
Once the application is running (at http://localhost:8080/graphql
), you can test the GraphQL API using tools like:
All GraphQL requests to this API must include a valid X-API-KEY
header for authentication. This API key serves as a security mechanism to restrict unauthorized access to the protected resources. When testing queries or mutations using tools like Postman or GraphQL Playground, make sure to add the following header to every request:
Headers:
{
"X-API-KEY":"4e1f2d3a4b5c6d7e8f9g0h1i2j3k4l5m6n7o8p9a"
}
Note: If the API key is missing or invalid, the server will respond with an authentication error and deny access to the requested operation.
Missing API Key Response:
{
"error": {
"error": "Unauthorized: Missing API key"
}
}
Invalid API Key Response:
{
"error": {
"error": "Unauthorized: Invalid API key"
}
}
All API operations are served through a single HTTP endpoint:
http://localhost:8080/graphql
GraphQL Mutation:
mutation SaveDepartment {
saveDepartment(
input: { id: "d011", deptName: "Security", createdBy: "1", active: true }
) {
id
deptName
active
}
}
Successful Response:
{
"data": {
"saveDepartment": {
"id": "d011",
"deptName": "Security",
"active": true
}
}
}
GraphQL Mutation:
mutation SaveDepartment {
saveDepartment(
input: { id: "d011", deptName: "Security", createdBy: "1", active: true }
) {
id
deptName
active
}
}
Successful Response:
{
"data": {
"saveDepartment": {
"id": "d011",
"deptName": "Security",
"active": true
}
}
}
📝 Note:
The system internally handles cases where the provided department id
already exists in the database:
- The system will fetch the existing department record with the given ID.
- It will then update the existing record's details using the values provided in the GraphQL mutation (
deptName
,active
, etc.). - The
updatedBy
field will be set using the value ofcreatedBy
from the request. - If the ID does not exist, a new record will be created as usual.
This approach provides a form of idempotent upsert behavior, where the request can act as either a create or update depending on the presence of the record.
GraphQL Query:
query GetDepartmentById {
getDepartmentById(id: "d011") {
id
deptName
active
}
}
Successful Response:
{
"data": {
"getDepartmentById": {
"id": "d011",
"deptName": "Security",
"active": true
}
}
}
GraphQL Mutation:
mutation UpdateDepartment {
updateDepartment(
id: "d011"
input: { deptName: "Operation", active: true, updatedBy: "2" }
) {
id
deptName
active
}
}
Successful Response:
{
"data": {
"updateDepartment": {
"id": "d011",
"deptName": "Operation",
"active": true
}
}
}
GraphQL Mutation:
mutation DeleteDepartment {
deleteDepartment(id: "d011")
}
Successful Response:
{
"data": {
"deleteDepartment": true
}
}
GraphQL Mutation:
mutation SaveEmployee {
saveEmployee(
input: {
birthDate: "1990-08-01"
firstName: "Michael"
lastName: "jordan"
gender: "M"
hireDate: "2000-01-01"
active: true
createdBy: "1"
departments: [
{
departmentId: "d001",
fromDate: "2001-01-01",
toDate: "2005-03-30"
}
]
salaries: [
{
amount: 60116,
fromDate: "2001-01-01",
toDate: "2005-12-31"
}
]
titles: [
{
title: "Senior Engineer"
fromDate: "2001-01-01"
toDate: "2005-12-31"
}
]
}
) {
id
birthDate
firstName
lastName
gender
hireDate
active
departments {
departmentId
fromDate
toDate
}
salaries {
amount
fromDate
toDate
}
titles {
title
fromDate
toDate
}
}
}
Successful Response:
{
"data": {
"saveEmployee": {
"id": 11,
"birthDate": "1990-08-01",
"firstName": "Michael",
"lastName": "jordan",
"gender": "M",
"hireDate": "2000-01-01",
"active": true,
"departments": [
{
"departmentId": "d001",
"fromDate": "2001-01-01",
"toDate": "2005-03-30"
}
],
"salaries": [
{
"amount": 60116,
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
}
],
"titles": [
{
"title": "Senior Engineer",
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
}
]
}
}
}
GraphQL Query:
query GetEmployeeById {
getEmployeeById(id: "11") {
id
birthDate
firstName
lastName
gender
hireDate
active
departments {
departmentId
fromDate
toDate
}
salaries {
amount
fromDate
toDate
}
titles {
title
fromDate
toDate
}
}
}
Successful Response:
{
"data": {
"getEmployeeById": {
"id": 11,
"birthDate": "1990-08-01",
"firstName": "Michael",
"lastName": "jordan",
"gender": "M",
"hireDate": "2000-01-01",
"active": true,
"departments": [
{
"departmentId": "d001",
"fromDate": "2001-01-01",
"toDate": "2005-03-30"
}
],
"salaries": [
{
"amount": 60116,
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
}
],
"titles": [
{
"title": "Senior Engineer",
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
}
]
}
}
}
GraphQL Mutation:
mutation UpdateEmployee {
updateEmployee(
id: 11
input: {
birthDate: "1990-08-01"
firstName: "Michael"
lastName: "jordan"
gender: "M"
hireDate: "2000-01-01"
active: true
createdBy: "1"
departments: [
{
departmentId: "d001",
fromDate: "2001-01-01",
toDate: "2005-03-30"
}
{
departmentId: "d002",
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
salaries: [
{
amount: 60116,
fromDate: "2001-01-01",
toDate: "2005-12-31"
}
{
amount: 78010,
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
titles: [
{
title: "Senior Engineer"
fromDate: "2001-01-01"
toDate: "2005-12-31"
}
{
title: "Lead Engineer",
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
}
) {
id
birthDate
firstName
lastName
gender
hireDate
active
departments {
departmentId
fromDate
toDate
}
salaries {
amount
fromDate
toDate
}
titles {
title
fromDate
toDate
}
}
}
Successful Response:
{
"data": {
"updateEmployee": {
"id": 11,
"birthDate": "1990-08-01",
"firstName": "Michael",
"lastName": "jordan",
"gender": "M",
"hireDate": "2000-01-01",
"active": true,
"departments": [
{
"departmentId": "d001",
"fromDate": "2001-01-01",
"toDate": "2005-03-30"
},
{
"departmentId": "d002",
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
],
"salaries": [
{
"amount": 60116,
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
},
{
"amount": 78010,
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
],
"titles": [
{
"title": "Senior Engineer",
"fromDate": "2001-01-01",
"toDate": "2005-12-31"
},
{
"title": "Lead Engineer",
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
]
}
}
}
4. Remove one child entity (e.g., one salary history record) during update and ensure proper orphan removal
GraphQL Mutation:
mutation UpdateEmployee {
updateEmployee(
id: 11
input: {
birthDate: "1990-08-01"
firstName: "Michael"
lastName: "jordan"
gender: "M"
hireDate: "2000-01-01"
active: true
createdBy: "1"
departments: [
{
departmentId: "d002",
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
salaries: [
{
amount: 78010,
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
titles: [
{
title: "Lead Engineer",
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
}
) {
id
birthDate
firstName
lastName
gender
hireDate
active
departments {
departmentId
fromDate
toDate
}
salaries {
amount
fromDate
toDate
}
titles {
title
fromDate
toDate
}
}
}
Successful Response:
{
"data": {
"updateEmployee": {
"id": 11,
"birthDate": "1990-08-01",
"firstName": "Michael",
"lastName": "jordan",
"gender": "M",
"hireDate": "2000-01-01",
"active": true,
"departments": [
{
"departmentId": "d002",
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
],
"salaries": [
{
"amount": 78010,
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
],
"titles": [
{
"title": "Lead Engineer",
"fromDate": "2006-01-01",
"toDate": "2007-03-30"
}
]
}
}
}
GraphQL Mutation:
mutation UpdateEmployee {
updateEmployee(
id: 11
input: {
birthDate: "1990-08-01"
firstName: "Michael"
lastName: "jordan"
gender: "M"
hireDate: "2000-01-01"
active: true
createdBy: "1"
departments: [
{
departmentId: "INVALID_DEPARTMENT"
fromDate: "2006-01-01"
toDate: "2007-03-30"
}
]
salaries: [
{
amount: 78010,
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
titles: [
{
title: "Lead Engineer",
fromDate: "2006-01-01",
toDate: "2007-03-30"
}
]
}
) {
id
birthDate
firstName
lastName
gender
hireDate
active
departments {
departmentId
fromDate
toDate
}
salaries {
amount
fromDate
toDate
}
titles {
title
fromDate
toDate
}
}
}
Successful Response:
{
"errors": [
{
"message": "Department not found: INVALID_DEPARTMENT",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"updateEmployee"
],
"extensions": {
"classification": "DataFetchingException"
}
}
],
"data": {
"updateEmployee": null
}
}
GraphQL Mutation:
mutation DeleteEmployee {
deleteEmployee(id: "11")
}
Successful Response:
{
"data": {
"deleteEmployee": true
}
}
- This project is a backend-focused API, intended to manage Employees and their relationships with Departments, Salaries, and Titles using GraphQL.
- All operations are protected by a lightweight API key mechanism using the
X-API-KEY
header. - The schema and DTOs are kept clean and follow GraphQL best practices for separation of input and output types.
- DTO input fields are validated using Java Bean Validation (JSR-380) annotations such as
@NotBlank
,@NotNull
, and custom constraints to ensure data integrity at the API level. - Validation errors are consistently handled through a custom
GraphQLExceptionConfig
, which transforms exceptions into structured and informative GraphQL error responses. - Clean architecture and clear separation of concerns are implemented using service layers, DTO mappings, and repository abstraction.
- OAuth2 / JWT Security Integration
Replace the static API key mechanism with OAuth2 or JWT-based authentication using Spring Security. This will enhance scalability, token expiration control, and multi-user access management.
- GraphQL Subscriptions
Implement GraphQL subscriptions to enable real-time notifications when an employee is created, updated, or deleted. This would be useful for reactive client-side applications.
- REST API + PostgreSQL Repository, check out REST API Spring Boot One To Many example with Hibernate and PostgreSQL.
- REST API + JWT Authentication Repository, check out Netflix Shows REST API with JWT Authentication.