A RESTful API for managing person data, built with Kotlin, Ktor, Ktor native DI, Exposed, PostgreSQL, and Meilisearch.
This project follows Domain-Driven Design (DDD) principles and Clean Architecture patterns:
- Domain Layer: Contains the core business logic, entities, and interfaces.
- Application Layer: Orchestrates the flow of data and coordinates the domain layer.
- Infrastructure Layer: Provides implementations for interfaces defined in the domain layer.
- Interface Layer: Handles HTTP requests and responses.
- CRUD operations for Person entities
- Address management
- Domain events for tracking changes
- JDK 21 or higher
- PostgreSQL 17.5
- Meilisearch 1.14.0
-
Copy the environment template file:
cp .env.sample .env -
Edit the
.envfile and update the values with your actual API keys:MEILISEARCH_API_KEY: Your Meilisearch API keyMEILI_MASTER_KEY: Your Meilisearch master keyDD_API_KEY: Your Datadog API keyDD_ENV: The deployment environment for Datadog (e.g., local, dev, prod)DD_VERSION: The application version reported to Datadog (e.g., 0.0.1)
Note: The
.envfile is ignored by git to keep your sensitive keys secure. Use local overrides or secret managers for production environments.
-
Start the entire application stack using docker-compose:
docker-compose up -dThis will start:
- The Person API application on port 8080
- PostgreSQL 17.5 on port 5432 with database name "person", username "postgres", and password "postgres"
- Meilisearch 1.14.0 on port 7700 with your configured master key
- Meilisearch UI on port 3000
-
Alternatively, you can build and run just the application locally:
./gradlew runNote: If you're running the application locally while using Docker for the dependencies, you'll need to configure the application to use the correct connection details for PostgreSQL and Meilisearch.
The application configuration can be overridden using the following environment variables:
- PORT: The port on which the Ktor server will run (default: 8080)
- DATABASE_URL: JDBC URL for the PostgreSQL database (default: jdbc:postgresql://localhost:5432/person)
- DATABASE_USER: Username for the PostgreSQL database (default: postgres)
- DATABASE_PASSWORD: Password for the PostgreSQL database (default: postgres)
- MEILISEARCH_URL: URL for the Meilisearch server (default: http://localhost:7700)
- MEILISEARCH_API_KEY: API key for the Meilisearch server (configured in .env file)
- MEILI_MASTER_KEY: Master key for the Meilisearch server (configured in .env file)
- DD_API_KEY: API key for Datadog monitoring (configured in .env file)
- DD_ENV: Deployment environment tag for Datadog (e.g., local, dev, prod)
- DD_VERSION: Application version tag for Datadog traces/logs (e.g., 0.0.1)
Alternatively, you can start the services individually:
-
Start PostgreSQL:
docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=person postgres:17.5 -
Start Meilisearch:
docker run -d --name meilisearch -p 7700:7700 getmeili/meilisearch:v1.14.0 -
Build and run the application:
./gradlew run
The API will be available at http://localhost:8080.
GET /api/persons- Get all persons (both individuals and legal entities)GET /api/persons/{id}- Get a person by IDDELETE /api/persons/{id}- Delete a person
POST /api/persons/individuals- Create a new individualPUT /api/persons/individuals/{id}- Update an individual
POST /api/persons/legal-entities- Create a new legal entityPUT /api/persons/legal-entities/{id}- Update a legal entity
POST /api/persons/{id}/addresses- Add an address to a personDELETE /api/persons/{personId}/addresses- Remove an address from a person (using query parameters)
The API supports two types of persons: Individuals and Legal Entities.
Both Individual and Legal Entity types share the following common structure:
- ID (UUID)
- Type (INDIVIDUAL or LEGAL_ENTITY)
- Addresses (list)
An individual person has the following specific fields:
-
Personal Info:
- First Name
- Last Name
- Birth Date
- Birth City
- Birth Country
- Nationality
- Marital Status (enum: SINGLE, MARRIED, DIVORCED, WIDOWED, PACS, SEPARATED)
- Guardianship Type (enum: NONE, GUARDIANSHIP, CURATORSHIP)
-
Additional Information:
- Matrimonial Regime (enum: COMMUNITY_OF_PROPERTY, SEPARATION_OF_PROPERTY, COMMUNITY_OF_ACQUISITIONS, UNIVERSAL_COMMUNITY, PARTICIPATION_IN_ACQUISITIONS)
- Social Security Number
A legal entity has the following specific fields:
-
General Information:
- Company Name
- Commercial ID
- Identifier
- Mail Reference
- SIREN
- SIRET
- Legal Form
- Creation Date
-
Legal Information:
- Commerce Register Number
- Registration Date
- Registration Country
- Registration Place
- Fiscal Year End Date
- Employee Count
- APE Code
- Activity Sector
- Share Capital
-
Legal Representative:
- Name
- Position
- Phone
- Start Date
- End Date (optional)
Both person types can have multiple addresses with the following fields:
- ID (UUID)
- Type (enum: COMMUNICATION, HOME, WORK, OTHER)
- Number
- Street
- City
- Postal Code
- Phone (optional)
- Email (optional)
- Is Secondary (boolean)
- Is Undeliverable (boolean)
- Validity Start Date (optional)
- Validity End Date (optional)
POST /api/persons/individuals
Content-Type: application/json
{
"personalInfo": {
"firstName": "John",
"lastName": "Doe",
"birthDate": "1980-01-15",
"birthCity": "New York",
"birthCountry": "United States",
"nationality": "American",
"maritalStatus": "MARRIED",
"guardianshipType": "NONE"
},
"additionalInformation": {
"matrimonialRegime": "COMMUNITY_OF_PROPERTY",
"socialSecurityNumber": "123-45-6789"
},
"addresses": [
{
"type": "HOME",
"number": "123",
"street": "Main Street",
"city": "Paris",
"postalCode": "75001",
"phone": "+33123456789",
"email": "john.doe@example.com",
"isSecondary": false,
"isUndeliverable": false
}
]
}POST /api/persons/legal-entities
Content-Type: application/json
{
"generalInformation": {
"companyName": "Acme Corporation",
"commercialId": "ACME2023",
"identifier": "AC-12345",
"mailReference": "ACME-MAIL-REF",
"siren": "123456789",
"siret": "12345678900012",
"legalForm": "SAS",
"creationDate": "2010-03-25"
},
"legalInformation": {
"commerceRegisterNumber": "RCS PARIS B 123 456 789",
"registrationDate": "2010-03-30",
"registrationCountry": "France",
"registrationPlace": "Paris",
"fiscalYearEndDate": "12-31",
"employeeCount": 42,
"apeCode": "6201Z",
"activitySector": "Computer programming activities",
"shareCapital": "100000€"
},
"legalRepresentative": {
"name": "Jane Smith",
"position": "CEO",
"email": "jane.smith@acme.com",
"phone": "+33123456789",
"startDate": "2015-01-01"
},
"addresses": [
{
"type": "COMMUNICATION",
"number": "456",
"street": "Business Avenue",
"city": "Paris",
"postalCode": "75008",
"phone": "+33987654321",
"email": "contact@acme.com",
"isSecondary": false,
"isUndeliverable": false
}
]
}PUT /api/persons/individuals/{id}
Content-Type: application/json
{
"personalInfo": {
"firstName": "John",
"lastName": "Doe",
"birthDate": "1980-01-15",
"birthCity": "New York",
"birthCountry": "United States",
"nationality": "American",
"maritalStatus": "DIVORCED",
"guardianshipType": "NONE"
},
"additionalInformation": {
"matrimonialRegime": null,
"socialSecurityNumber": "123-45-6789"
}
}PUT /api/persons/legal-entities/{id}
Content-Type: application/json
{
"generalInformation": {
"companyName": "Acme Corporation",
"commercialId": "ACME2023-UPDATED",
"identifier": "AC-12345",
"mailReference": "ACME-MAIL-REF-NEW",
"siren": "123456789",
"siret": "12345678900012",
"legalForm": "SAS",
"creationDate": "2010-03-25"
},
"legalInformation": {
"commerceRegisterNumber": "RCS PARIS B 123 456 789",
"registrationDate": "2010-03-30",
"registrationCountry": "France",
"registrationPlace": "Paris",
"fiscalYearEndDate": "12-31",
"employeeCount": 50,
"apeCode": "6201Z",
"activitySector": "Computer programming activities",
"shareCapital": "150000€"
},
"legalRepresentative": {
"name": "Jane Smith",
"position": "CEO",
"email": "jane.smith@acme.com",
"phone": "+33123456789",
"startDate": "2015-01-01"
}
}POST /api/persons/{id}/addresses
Content-Type: application/json
{
"type": "COMMUNICATION",
"number": "456",
"street": "Business Avenue",
"city": "Paris",
"postalCode": "75008",
"phone": "+33987654321",
"email": "contact@example.com",
"isSecondary": false,
"isUndeliverable": false,
"validityStartDate": "2023-01-01T00:00:00Z"
}DELETE /api/persons/{personId}/addresses?type=HOME&streetNumber=123&streetName=Main%20Street&city=Paris&postalCode=75001This repository uses Cocogitto (cog) to enforce Conventional Commits, automate versioning, and generate the changelog.
- Why: consistent commit messages -> automated semantic versioning and clean release notes
- Config: see
cog.tomlat the project root (changelog is written toCHANGELOG.md, CI skip token is[skip ci])
- macOS (Homebrew):
brew install cocogitto - Using Cargo (Rust):
cargo install cocogitto-cli - Prebuilt binaries: https://github.com/cocogitto/cocogitto/releases
<type>(<optional scope>)!: <short summary>
Examples:
feat(person): add endpoint to create legal entities
fix(address): normalize postal code before persistence
docs: add API usage examples
refactor: extract address parsing utility
test: add IBAN validation tests
chore: upgrade dependencies
feat!: remove deprecated address fields (BREAKING)
Breaking changes can also be declared in the footer:
BREAKING CHANGE: details about the breaking change
- Check the history:
cog check - Verify a specific commit message file (used by the hook):
cog verify --file .git/COMMIT_EDITMSG
A commit-msg hook is defined in cog.toml and will:
- verify the commit message format
- run
cog checkto ensure the history stays valid
Install the hook:
- Preferred:
cog install-hook --all - If the above is not available in your version, create
.git/hooks/commit-msg(chmod +x) with a script that runs:
cog verify --file "$1" && cog check- Auto bump based on commits since last tag:
cog bump --auto - Force a bump:
cog bump patch | minor | major - Generate/preview changelog:
cog changelog - After a bump, a tag and
CHANGELOG.mdentry are generated according tocog.toml
- To skip CI for a commit, include
[skip ci]in the commit message (configured incog.toml).