콘텐츠로 건너뛰기

How DDD Concept can be applied to Project

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

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 use TransactionRepository, and domain’s function CreateTransaction. 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 seen CreateTransaction 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 library yggdrasill which helps save data to level-db. In Save function you can see br.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 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

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 and command is located inside each component, the other component should reference other component’s event and command type for communication with other component. And it looks like we broke up the bounded context.
  • Before event and command using primitive type, they use its own domain type inside. And this feel we completely broke up bounded contexts, for example, if blockchain component wants to communicate with txpool component blockchain component should know about txpool domain type because blockchain should make txpool‘s event or command 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.

Leave a comment