Building Modular Monolith Core Application Logic With Rust

Hiraq Citra M
lifefunk
9 min readJun 22, 2024

--

I have a midnight project that I’ve been working on now building the personal computation for the DID (Decentralized Identifier). The code repository:

This project is still in active development now, but as a short explanation, this project is called Vessel it’s like a personal container that should be able to deploy on a cloud environment used to maintain personal identity, which is in this case implementing the DID. This personal container is like Pod from the Solid Project :

Short explanation aboutSolid Pod :

Entities control access to the data in their Pod. Entities decide what data to share and with whom (be those individuals, organizations, applications, etc.), and can revoke access at any time.

About DID

Short explanation about this concept:

Decentralized identifiers (DIDs) are a new type of identifier that enables verifiable, decentralized digital identity

Unlike other identity concepts which the identity owned and maintained by third-party services or companies such as Facebook, Google, or others, this concept (DID), introduces a new concept in which a user or person will be able to own and maintain their identity.

This concept has a component called Identity Hub or Identity Agent :

Identity hubs, also known as identity agents, act as intermediaries between users and external entities in a decentralized identity ecosystem.

Source:

The Vessel is a personal container that will be deployed, owned, and maintained by a single user one of its functionalities is to maintain DID Personal Agent.

Modular Monolith

I am interested in this architecture concept and have already learned and researched it for a long time, and now through my own personal project, I have a chance to implement this concept.

There is a YouTube video that I’ve watched multiple times that I think gives a solid foundation as to why this architecture matters:

And I’m also recommend this article to read too:

The reason why I’m interested in this architecture is that, although I’ve been working on microservices architecture for years, I’ve realized that it will be better for us as software engineers to be able to build a better monolith with good modular concepts rather than thinking that building separate services will solve problems such as code structures, domain separation, and others. If we’re not able to solve all of these problems in the single monolith unit, we should not be thinking that we were able to solve them by implementing microservices.

Thinking and building in Modular Monolith will force us to think about building a separation of domains. Each of the available domains will be grouped in a single module and this module should be isolated from others, and even better this module should be able to test, run, and even deploy as a single unit separated from others

DDD (Domain Driven Design)

As I’ve mentioned earlier, that building applications in Modular Monolith will force us to group a set of codes into something. The question is, how should define this thing? That is the reason why I’m using DDD (Domain Driven Design) . By thinking and implementing this concept we will have a set of concepts that have already proven to determine and manage a set of knowledge of something.

Some of the concepts that I’ve already used and implemented to my personal project ( Vessel ):

  • Domain Entity
  • Domain Service
  • Domain API
  • Repository Pattern

Later I’ll show you how to manage and structure these things.

Core Application Logic

If you come to this article and look in more detail at the image above, it just shows you an image of an application architecture that was built using Hexagonal Architecture. One thing that I really like from this pattern is a pattern that separates between application infrastructure and application core logic.

What is the core application logic? The short answer is it’s about an abstraction of business logic. This concept doesn’t need to think about the implementation details or infrastructure implementation such as, not caring about the REST API/gRPC or database implementation such as RDBMS vs NoSQL. It’s all about the business logic.

I have a package called vessel-core which contains all core application logic for my personal project Vessel . This package contains a set domain abstraction from multiple domain logics. It is just pure abstraction and there are no implementation details embedded inside of it.

Rust Implementation

Code Structure

This is the structure that I’ve built for my personal project

./vessel-core
├── Cargo.toml
└── src
├── identity
│ ├── account
│ │ ├── account.rs
│ │ ├── mod.rs
│ │ ├── types.rs
│ │ ├── uri.rs
│ │ └── usecase.rs
│ ├── mod.rs
│ └── verifiable
│ ├── credential
│ │ ├── credential.rs
│ │ ├── holder.rs
│ │ ├── mod.rs
│ │ ├── types.rs
│ │ └── usecase.rs
│ ├── mod.rs
│ ├── presentation
│ │ ├── mod.rs
│ │ ├── presentation.rs
│ │ ├── types.rs
│ │ └── usecase.rs
│ ├── proof
│ │ ├── builder.rs
│ │ ├── mod.rs
│ │ └── types.rs
│ └── types.rs
└── lib.rs

For now, there is only a single domain which is identity . This is a root domain which has multiple sub-domains inside it:

  • account
  • verifiable
  • verifiable/credential
  • verifiable/presentation

All of these sub-domains are called as a module. This is how I create and maintain a module, which is based on domain knowledge of DID (Decentrlaized Identity) .

DDD Implementation

Now, I’ll show an example of implementation of how I implement some of DDD concepts:

  • Domain Entity
  • Domain Service
  • Domain API
  • Repository Pattern

Let’s take it from the identity/account module. The entity will be like this:

/// `Account` is main entity data structure
///
/// This entity will able to define user/person, organization
/// machine, everything. For the non-human data identity, it should
/// has it's own controller
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "self::serde")]
pub struct Account {
pub(crate) id: String,
pub(crate) did: String,
pub(crate) doc: Doc,
pub(crate) doc_private_keys: IdentityPrivateKeyPairs,
pub(crate) keysecure: KeySecure,

#[serde(with = "ts_seconds")]
pub(crate) created_at: DateTime<Utc>,

#[serde(with = "ts_seconds")]
pub(crate) updated_at: DateTime<Utc>,
}

The Account is a Domain Entity from the account module. If you see more detail, I’m also limiting the access visibility to its property fields, by giving it pub(crate) , the reason why I think that the access visibility is important is to prevent the outside world or outside entity from changing the state of the entity object. The business logic should be embedded inside the entity or aggregate root, to prevent us from facing an anemic domain model, which is an anti-pattern of DDD itself.

About the anemic domain model:

Now, let’s take an example implementation of Domain Service :

/// `URI` is a domain service that build specifically
/// for the `DID URI` management, it should be used to [`URI::build`] and [`URI::parse`]
pub struct URI;

impl URI {
/// `build` used to build the `DID URI` based on given [`Account`] and query params
pub fn build(
account: impl AccountEntityAccessor,
password: String,
params: Option<Params>,
) -> Result<String, AccountError> {
let did = DID::from_keysecure(password, account.get_keysecure())
.map_err(|err| AccountError::ResolveDIDError(err.to_string()))?;

did.build_uri(params)
.map_err(|err| AccountError::BuildURIError(err.to_string()))
}

/// `parse` used to parse given `DID URI` used to parse and generate [`Multiaddr`], [`Params`]
/// and `DID Account URI`
pub fn parse(uri: String) -> Result<(Multiaddr, Params, String), AccountError> {
let (did_uri, uri_params) =
DID::parse_uri(uri).map_err(|err| AccountError::ResolveDIDError(err.to_string()))?;

if uri_params.hl.is_none() {
return Err(AccountError::ResolveDIDError(
"invalid hashlink value".to_string(),
));
}

let parsed_addr = uri_params
.parse_multiaddr()
.map_err(|err| AccountError::ResolveDIDError(err.to_string()))?
.ok_or(AccountError::ResolveDIDError(
"unable to parse MultiAddress format".to_string(),
))?;

Ok((parsed_addr, uri_params, did_uri))
}
}

What is Domain Service ? It’s a place where to locate any logic that is not suited for the Entity or ValueObject . I have this object, URI , which is used to build and parse a DID URI.

Now, let’s take an example of Domain API :

#[async_trait]
pub trait AccountAPI: Clone {
type EntityAccessor: AccountEntityAccessor;

/// `generate_did` used to geenerate new `DID Account`
///
/// This method will depends on two parameters:
/// - `password`
///
/// The `password` used to save the generated private key pair into encrypted
/// storage data structure. This strategy following `Ethereum KeyStore` mechanism.
/// This property will be used to generate hash that will be used as a key to encrypt
/// and decrypt the generated private key
async fn generate_did(&self, password: String) -> Result<Self::EntityAccessor, AccountError>;

/// `build_did_uri` used to generate the `DID URI`, a specific URI syntax for the DID
///
/// Example
///
/// ```text
/// did:prople:<base58_encoded_data>?service=peer&address=<multiaddr_format>&hl=<hashed_link>
/// ```
async fn build_did_uri(
&self,
did: String,
password: String,
params: Option<Params>,
) -> Result<String, AccountError>;

/// `resolve_did_uri` used to resolve given `DID URI` and must be able to return `DID DOC`
/// by calling an `JSON-RPC` method of `resolve_did_doc` to oher `Vessel Agent`
async fn resolve_did_uri(&self, uri: String) -> Result<Doc, AccountError>;

/// `resolve_did_doc` used to get saved `DID DOC` based on given `DID Account`
async fn resolve_did_doc(&self, did: String) -> Result<Doc, AccountError>;

/// `remove_did` used to remove saved [`Account`] based on given `DID`
async fn remove_did(&self, did: String) -> Result<(), AccountError>;

/// `get_account_did` used to load data [`Account`] from its persistent storage
async fn get_account_did(&self, did: String) -> Result<Self::EntityAccessor, AccountError>;
}

What is Domain API ? In reality, when we have multiple domains available in our application, sometimes each of those domains will need to communicate with each other. The concept of Domain API actually is an abstraction or an interface that is used to communicate. The communication between domains should be through the domain interfaces.

Example of domain API integration:

pub trait UsecaseBuilder<TAccountEntity, TCredentialEntity, THolderEntity>:
CredentialAPI<EntityAccessor = TCredentialEntity>
where
TAccountEntity: AccountEntityAccessor,
TCredentialEntity: CredentialEntityAccessor,
THolderEntity: HolderEntityAccessor,
{
type AccountAPIImplementer: AccountAPI;
type RepoImplementer: RepoBuilder<
CredentialEntityAccessor = TCredentialEntity,
HolderEntityAccessor = THolderEntity,
>;
type RPCImplementer: RpcBuilder;

fn account(&self) -> Self::AccountAPIImplementer;
fn repo(&self) -> Self::RepoImplementer;
fn rpc(&self) -> Self::RPCImplementer;
}

This is taken from the identity/verifiable/credential which is an interface of UsecaseBuilder that depends on AccountAPI . This means, this interface or abstraction from the Credential sub-domain or module, needs to communicate with Account through AccountAPI.

The UsecaseBuilder is an abstraction that will be used for the application’s level controller. Each application controller will use this use-case object that contains the core application logic.

The diagram will be like this:

And for the last is about the Repository Pattern. This is an example of implementation of it:

#[async_trait]
pub trait RepoBuilder: Clone + Sync + Send {
type CredentialEntityAccessor: CredentialEntityAccessor;
type HolderEntityAccessor: HolderEntityAccessor;

async fn save_credential(
&self,
data: &Self::CredentialEntityAccessor,
) -> Result<(), CredentialError>;
async fn save_credential_holder(
&self,
data: &Self::HolderEntityAccessor,
) -> Result<(), CredentialError>;
async fn remove_credential_by_id(&self, id: String) -> Result<(), CredentialError>;
async fn remove_credential_by_did(&self, did: String) -> Result<(), CredentialError>;

async fn get_credential_by_id(
&self,
id: String,
) -> Result<Self::CredentialEntityAccessor, CredentialError>;

async fn list_credentials_by_ids(
&self,
ids: Vec<String>,
) -> Result<Vec<Self::CredentialEntityAccessor>, CredentialError>;

async fn list_credentials_by_did(
&self,
did: String,
pagination: Option<PaginationParams>,
) -> Result<Vec<Self::CredentialEntityAccessor>, CredentialError>;
}

The Repository Pattern is an abstraction used to map our Domain Entity with the persistent storage. This abstraction will be used inside our UsecaseBuilder .

Outro

I’ve tried to share my experiences when working on my personal project and implementing Modular Monolith Architecture , DDD (Domain Driven Design), and how to implement it using Rust .

Building a set of abstractions in Rust is really fun for me. It already has a set of features that will help us to maintain an abstraction, and almost like Java or Scala but with better performance, since it also gives us a zero-cost abstraction, which means we will not get any performance effects or performance penalty due to the abstraction.

The Rust Workspace also gives me a really great tool to maintain the code structure since I’m able to maintain multiple separate packages independently.

The conclusion is, that building the modular monolith with Rust is really recommended.

--

--