I’ve translated blog post (korean version) which is about 7 important concepts which is about DDD(Domain-Driven Design).
- Ubiquitous language
- Layers
- Bounded contexts
- Anti-Corruption Layer
- Shared Kernal
- Generic subdomain
After translating post I’ve done projects which apply DDD concepts. One is blockchain engine project, it-chain. You can see the codes on this link
- Lightweight & Customizable blockchain engine, it-chain: https://github.com/it-chain/engine
In this post, I’m going to show how DDD concepts can be applied to real-world project codes and code snippets which will be shown in this post are based on it-chain project I’ve introduced above.
Ubiquitous language
Ubiquitous language is a language that define terms which are matching business requirements on Application and technology for implementing it. For example, blockchain
component in it-chain project needs to manage blocks (save blocks, create blocks…). Managing blocks is business requirements for blockchain
component. But the problem is the block is not all the same, there can be several different states on each block. For example, there can be the block which is just created with transactions, this block is not saved to repository and even not concensused with members of network. This ‘just created’ block should not be treated as block which is consensused successfully by members and saved to repository.
So the developers who are work on blockchain
component think that they need to define terms about block states: ‘Created’, ‘Staged’, ‘Commited’. Then why did they defined those terms? One problem when developers work on big project, they cannot understand the codes developed by others easily. Even worse, one developer may misunderstand the code! This problem will getting worse as code base grow.
By defining common language(Ubiquitous language) before work on codes or before develop some features, the other developers can understand what this code is doing now and why this code is placed here. it-chain developers defined term ‘Created’, ‘Stateged’, ‘Commited’ right after they think they need to distinguish block states. Next time as new contributor came in blockchain
component and looked at CreatedBlock
in method. He/She can easily figure out ‘Oh, this method is about just created block which is not consensused and saved to repository’. Because we defined common language about block states.
Layers
Layering concept is used in other designs, but I imported several layers identified by DDD:
User Interface
User interface responsible for draw screen which makes user to interface with Application, convert user’s input into Application commands. Important point here is that user in User Interface is not human. But Application also can be user if that Application use the other Application’s API.
In it-chain project, we can see User Interface layer in
cmd
package.// /cmd/connection/join.go func Join() cli.Command { return cli.Command{ Name: "join", Usage: "it-chain connection join [node-ip-of-the-network]", Action: func(c *cli.Context) error { nodeIp := c.Args().Get(0) return join(nodeIp) }, } } func join(ip string) error { joinNetworkCommand := command.JoinNetwork{ Address: ip, } err := client.Call("connection.join", joinNetworkCommand, ...) return nil }
This code is about joining peer node to existing network. You can see it just receive user’s input (node ip addresss) and call rpc
client.Call
for joining into network. We cannot see application service logic, it just receives and then pass.
Application Layer
Application Layer orchestrate domain objects for carrying out Application business. So Application Layer should not contain business logic.
// /txpool/api/transaction_api.go type TransactionApi struct { nodeId string transactionRepository txpool.TransactionRepository ... } func (t TransactionApi) CreateTransaction(txData txpool.TxData) (txpool.Transaction, error) { transaction, err := txpool.CreateTransaction(t.nodeId, txData) ... err = t.transactionRepository.Save(transaction) return transaction, err }
This code is about txpool component
CreateTransaction
API function. As function name imply it just create transaction with tx data and save it to repository. And there’s no business logic,TransactionApi
useTransactionRepository
, and domain’s functionCreateTransaction
. Application Service is abstracted with domain object. All the detailed (business logic) encapsulated inside domain object, function.
Domain Layer
As DDD(‘Domain driven development’) name says domain layer is key part of Application. Domain object such as service, repository, factory, model contains all the business logic.
// /txpool/transaction.go type Transaction struct { ID TransactionId TimeStamp time.Time Jsonrpc string ICodeID string Function string Args []string Signature []byte PeerID string } func CreateTransaction(publisherId string, txData TxData) (Transaction, error) { id := xid.New().String() timeStamp := time.Now() transaction := Transaction{ ID: id, PeerID: publisherId, TimeStamp: timeStamp, ICodeID: txData.ICodeID, Jsonrpc: txData.Jsonrpc, Signature: txData.Signature, Args: txData.Args, Function: txData.Function, } return transaction, nil }
This code is about txpool domain layer
Transaction
,CreateTransaction
. We’ve seenCreateTransaction
domain function is used inside Application Layer (TransactionApi
).CreateTransaction
says how to create transaction with txData. We can see all the detail business logic is encapsulated into Domain layer.Infrastructure
Infrastructure layer contains the techinical capabilities which support the layers above.
// /blockchain/infra/repo/block_repository.go type BlockRepository struct { mux *sync.RWMutex yggdrasill.BlockStorageManager } func (br *BlockRepository) Save(block blockchain.DefaultBlock) error { br.mux.Lock() defer br.mux.Unlock() err := br.BlockStorageManager.AddBlock(&block) if err != nil { return ErrAddBlock } return nil }
This code is about blockchain component infrastructure layer. One easy example of infrastructure layer is database.
BlockRepository
have database libraryyggdrasill
which helps save data to level-db. InSave
function you can seebr.BlockStorage.AddBlock(&block)
which works as wrapper for external library and helps to carrying out blockchain component Application business.
Bounded Contexts
As Application’s domain grow, there can be more developers who work on the same code base. But this situation have problems. As developers who work on same code base grow, the codes each developer should understand larger and understanding may can be hard. And this increases the possibility of bugs or errors. Furthermore, as code base developers work on grow, managing each developer’s work can be hard.
One way to solve these problems is separating one huge code base and “bounded context” help this. Bounded context is context which can separate domains based on its own concern.

it-chain logical architecture
it-chain project separate whole domain into several bounded contexts based on its own concern. and we called each bounded context as “Component”. it-chain has following components:
Client Gateway
Client gateway provides REST API for client application of it-chain
gRPC Gateway
gRPC gateway is service for communication for nodes of network. Communication needs for blockchain synchronize, consensus etc.
TxPool
TxPool temporarily save transactions which are not saved into block
Consensus
Consensus component is for consensus of block, currently Consensus component provides PBFT algorithm
Blockchain
Blockchain component helps to create, save block and synchronize blockchain
IVM
IVM component manage it-chain’s smart contract called iCode
Anti-Corruption Layer
Anti-corruption layer basically work as middleware between two different bounded context. So instead of each bounded context communicate directly, they use anti-corruption layer. Then why anti-corruption layer?
If two different component communicate directly without anti-corruption layer, one change in a component can affect the others. And this can be a disaster as project grows, a small fix in a component can break the whole system. What if we use anti-corruption layer and each component communicates with anti-corruption layer? A change in a component only affects anti-corruption layer, and if we communicate with anti-corruption layer with its interface, there may no affects on the other system except its own component!

it-chain logical architecture
In it-chain project each component only communicates with RabbitMQ Interface. And this helps developers make only cares about its own component. Nothing outside my component is my concern.
// /txpool/block_propose.service.go
func (b BlockProposalService) sendBlockProposal(transactions []Transaction) error {
ProposeBlockEvent := createProposeBlockCommand(transactions)
if err := b.eventService.Publish("block.propose", ProposeBlockEvent); err != nil {
...
return err
}
return nil
}
This code is about sending transactions from TxPool component to EventService. Instead of directly send transactions to Blockchain component, txpool publish ProposeBlockEvent
to message queue. So both txpool and blockchain don’t need to care about other side, just publish and subscribe.
Shared Kernel
However we separate whole system into bounded contexts, sometimes it is much more resonable to share some domain objects. With shared kernel each component strongly coupled with shared kernel but still make decoupled with the other components.
In it-chain project shared kernel is located in common
package. One big decision we made recently is that place event
and command
which are used to communicate different components into shared kernel (common
package) and every event
, command
must use primitive type.
- before
// TxPool propose transactions to Blockchain
package blockchain
type ProposeBlock struct {
BlockId string
TxList []blockchain.Transaction
}
- After
// TxPool propose transactions to Blockchain
// 1. place all the events and commands into common package (shared kernel)
package common
type ProposeBlock struct {
BlockId string
// 2. event and command type should use primitive type
TxList []common.Tx
}
type Tx struct {
ID string
ICodeID string
/* primitive type ... */
}
The reasons are as followed:
- Before
event
andcommand
is located inside each component, the other component should reference other component’sevent
andcommand
type for communication with other component. And it looks like we broke up the bounded context. - Before
event
andcommand
using primitive type, they use its own domain type inside. And this feel we completely broke up bounded contexts, for example, ifblockchain
component wants to communicate withtxpool
componentblockchain
component should know abouttxpool
domain type becauseblockchain
should maketxpool
‘sevent
orcommand
type.
Conclusion
In this post, we’ve looked at how DDD key concepts can be applied to real-world projects. With these examples, I hope you can get hint what is DDD.