This is a Demo project implementing various DDD patterns.
AMCRM is a backend implementation for customer management. It has a public REST api that is documented below.
Application has en embedded playground/documentation accessible at http://localhost:4567/docs by default.
Upon an error the backend returns an error object with the details when possible.
A typical error has the following structure:
Field | Type | Description |
---|---|---|
messsage | string | Human readable error message |
details | array[string] | A list of messages providing more details |
Generic error example:
HTTP/1.1 500 System error
Content-Type: application/json
{
"message": "Internal system error"
}
Validation error example:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"details": [
"$.id: is missing but it is required",
"$.name: is missing but it is required",
"$.surname: is missing but it is required"
],
"message": "Validation failed"
}
Authentication is session-based, once you have a session id you can perform requests to the restricted endpoints. Since Users resource is reserved only for admin, every user has a role: anonymous, user or admin. Based on the role the access is restricted.
Session expires in 1h but is automatically prolonged when used.
Session is sent in the Authorization
header:
GET /customers
Authorization: e059e8e2-585e-11ec-85f1-7f102f05f20c
...
To get a session id one can use a GitHub public oauth. An application needs to be created and client_id and client_secret configured.
- Get the login url
POST /oauth/github
HTTP/1.1 200 OK
{
"location": "https://github.com/login/oauth/authorize?client_id=1234567890"
}
- After the user comes back, the authentication in successful and their name is recognized a new session id is created and returned.
{
"sessionId":"cc0b9dea-585e-11ec-a20f-bf59e60f2403"
}
Initially the users can be created by the default admin that is created automatically during the start of application if there are no users.
It is possible to pass a limit and an offset for paginating the list results, which is not the best choice of course, something like a proper key set would be much more efficient, but it can be easily change when the need arises.
GET /customers?limit=100&page=2
HTTP/1.1 200 OK
Link: <{baseUrl}?limit=100&page=1>; rel="prev", <{baseUrl}?limit=100&page=3>; rel="next"
ListCustomers
GET /customers
HTTP/1.1 200 OK
Link: <{baseUrl}?limit={limit}&page={page}>; rel="next"
[
{
"id": "2",
"name": "Bill",
"surname": "Smith",
"photoLocation": null
},
...
]
CreateCustomer
POST /customers
{
"id": "2",
"name": "Bill",
"surname": "Smith",
"photo": "...base64 blob..."
}
HTTP/1.1 OK
{
"id": "2",
"name": "Bill",
"surname": "Smith",
"photoLocation": "{baseUrl}/customer/ee14bd14-74e2-4e19-8901-cba94525f8a9-32x32.jpg"
}
When calling this resource as admin additional information is provided, for example createdBy
.
GetCustomerDetails
GET /customers/{customerId}
HTTP/1.1 OK
{
"id": "2",
"name": "Bill",
"surname": "Smith",
"photoLocation": "{baseUrl}/customer/ee14bd14-74e2-4e19-8901-cba94525f8a9-32x32.jpg"
}
When calling this resource as admin additional information is provided, for example createdBy
.
PatchCustomer
PATCH /customers/{customerId}
{
"name": "Bill"
}
DeleteCustomer
DELETE /customers/{customerId}
ListUsers
GET /users
CreateUser
POST /users
GetUserDetails
GET /users/{userId}
ToggleUserAdminStatus
POST /users/{userId}/admin
DeleteUser
DELETE /users/{userId}
Build & Runtime requirements:
- JRE/JDK > 8
- SQLite database (optional)
- Docker (for Docker environments)
Application supports two storages:
- in-memory (not thread-safe, primarily for testing)
- database (transactional optimistic locking)
There is a default local filesystem photo storage included, but it can be easily extended to use some external file storage like AWS S3 or similar. In case of a local storage the path is saved in the domain storage and the base url is automatically prepended during the runtime.
By default, photos are saved into the public/
directory.
There is a maven wrapper available, so you don't have to rely on a system maven to
be present or to have a specific version. Instead of running mvn
run ./mvnw
.
-
Build
$ ./mvnw clean package
-
Configure
Configuration can be mixed, for example environment variables overwrite config file, which overwrites default values.
-
Defaults
- port: 4567
- storage provider: memory
-
Config file (YAML format)
port: 1234 storage: provider: database options: database: db.db oauth: client_id: "1234567890" client_secret: 12039d6dd9a7e27622301e935b6eefc78846802e
-
Environment variables
AMCRM_PORT=1234 AMCRM_STORAGE_PROVIDER=database AMCRM_STORAGE_DATABASE_OPTIONS=database=db.db AMCRM_OAUTH_CLIENT_ID=1234567890 AMCRM_OAUTH_CLIENT_SECRET=12039d6dd9a7e27622301e935b6eefc78846802e
-
-
OPTIONAL. Setup database (when using database storage)
$ sqlite3 db.db < src/main/resources/db.sql
-
Run
- Using a bash wrapper
$ bin/amcrm --config config.yml --port 4567 # or $ bin/amcrm --port 4567 # or $ AMCRM_PORT=1234 bin/amcrm
- Directly (it's an über-jar)
$ java -jar target/amcrm-1.0-SNAPSHOT.jar ...
- Build image
Building an image is a multistage process that utilizes buildkit and maven repository caching to speed things up.
$ bash build-docker.sh
# or
$ DOCKER_BUILDKIT=1 docker build . -t amcrm
-
Run container
The
public
directory is where the customers' photos will be uploaded. When not mapped they will be gone when the container stops.$ docker run --rm -p 4567:4567 -v /opt/amcrm/public:$PWD/public amcrm
Make sure before submitting a PR the code is properly formatted. This is done automatically by running the following
command (or install a git hook git config core.hooksPath .githooks
):
$ ./mvnw spotless:apply
Every time the database is changed we need to regenerate the code from the database:
$ sqlite3 db.db < src/main/resources/db.sql
$ mvn -P jooq package -DjooqDatabase=db.db
To build and run all the test suite run the following command:
$ ./mvnw clean package
Tests are broken down into:
- unit tests (mind the quotes)
$ ./mvnw test -D'groups=!integration,!functional'
- integration tests (tagged as "integration")
$ ./mvnw test -Dgroups=integration
- functional tests (tagged as "functional")
$ ./mvnw test -Dgroups=functional
The implementation follows Clean Architecture / Domain Driven Design approach. Thus, application is split into the following parts:
- domain
- infra
- api
Domain holds the domain logic of the application without relying on a specific storage or a framework. Domain enforces its own interfaces for different purposes that are implemented based on the need.
Domain contains of Entities, Events, Values Objects, Repositories & Commands.
- Entities: identifiable objects that implement business behavior.
- Events: events that happened in the domain (e.g. UserCreated).
- Value Objects: immutable structures.
- Repositories: storage abstractions.
- Commands: actions performed on domain objects to implement the business logic.
Infrastructure implements domain interfaces, provides specific helper classes like factories.
In addition, infrastructure holds Views that are the optimized read-only views for the storages. They can be used for viewing the domain objects through some mapping classes, building reports etc.
API is an adaptor that plugs into the domain and exposes different actions through REST API.
Resources are organized as Services with appropriate method mapping (e.g. service CustomerService
with listCutomers()
is mapped to GET /customers
).
Input validation is done by using JSON schemas.
Data from domain to the users is mapped by using DTOs (e.g. CustomerSummary).
jooq — is a thin abstraction over SQL that by using code generations guarantees type safety ( e.g. typos in the column names, wrong type mapping etc.).
armeria — is a Netty-based microservice framework that in addition to REST-like services allows building RPCs systems (Thrift, gRPC). It is event-driven, supports different metrics & tracing collection and provides a useful documentation/playground service out of the box.
jcommander — simplifies command line argument parsing.
rest-assured — DSL for building functional API tests.
jackson — swiss-army knife for JSON/YAML processing.