Applying DDD and Hexagonal Architecture to Build a Unified Danmaku Platform
The article explains how Bilibili’s senior engineer uses Domain‑Driven Design combined with hexagonal architecture to merge separate VOD and live‑stream Danmaku services into a unified platform, detailing sub‑domain decomposition, six‑sided ports, rich models, mapping logic, and the trade‑offs of improved modularity, maintainability, and reduced duplication versus added design and refactoring effort.
Author : Sun Jiaqi, senior development engineer at Bilibili.
Why adopt Domain‑Driven Design (DDD)? The article starts by listing typical pain points in growing business systems: heavy reliance on a few domain experts, difficulty understanding legacy code, painful hand‑overs, and escalating complexity as features multiply. These issues stem from a lack of systematic micro‑service frameworks, clear development standards, and procedural programming.
Complexity growth patterns are described as three models:
Coupling pattern – business logic spreads across unrelated modules, making debugging and testing hard.
Chimney (vertical silo) pattern – duplicated code for each new requirement, hindering component reuse.
Reuse pattern – static infrastructure is shared while dynamic business needs are added, keeping complexity growth sub‑linear.
The article then presents the business architecture design for a unified Danmaku platform that merges the previously separate VOD and live‑streaming Danmaku services. It outlines the gateway layer, business layer, platform component layer, and core interaction modules, showing before‑and‑after architecture diagrams (images omitted).
Strategic DDD design includes:
Dividing the domain into sub‑contexts (e.g., activity, resource, recommendation).
Using a hexagonal architecture combined with DDD layers to define clear responsibilities.
Adopting six‑sided ports and adapters to keep the domain core isolated.
Domain model example – Activity demonstrates a rich (active) model with validation and business methods:
type Activity struct {
ID int64
Name string
ClassifyId int32 // resource template id
State ActivityState
Type DimensionType // deployment type
Dimensions []*ActivityDimension
DynamicResource resource.DynamicResource
Ctime common.Time
Mtime common.Time
}
func (act *Activity) Validate() error {
for _, d := range act.Dimensions {
if act.ID != d.ActivityId || act.State != d.State || act.Type != d.Type {
return ecode.Error(ecode.ParamInvalid, "dimension和activity字段不匹配")
}
}
return act.DynamicResource.Validate()
}
func (act *Activity) HasUserBlackList() bool { return act.ClassifyId == resource.ClassifyActivityIcon }
func (act *Activity) HasCidBlackList() bool { return act.Type == dmActivity.Dimension_DimensionAll && act.ClassifyId == resource.ClassifyActivityIcon }Mapper example – converting DTO to DO shows how the mapper parses a JSON‑encoded resource, builds the appropriate concrete type, and validates the resulting domain object:
func ActivityFromDTO(dto *pb.DmActivity) (*entity.Activity, error) {
dynamicResource, err := parseResource(dto.ClassifyId, dto.GetResource())
if err != nil { return nil, err }
act := &entity.Activity{
ID: dto.GetId(),
Name: dto.GetName(),
ClassifyId: dto.GetClassifyId(),
State: entity.ActivityState(dto.GetState()),
Type: entity.DimensionType(dto.GetType()),
Dimensions: ActivityDimensionListFromDTO(dto.GetDimension()),
DynamicResource: dynamicResource,
Ctime: dto.GetCtime(),
Mtime: dto.GetMtime(),
}
if err = act.Validate(); err != nil { return nil, err }
return act, nil
}The resource parsing helper uses reflection to instantiate the correct concrete implementation based on a classification ID and then unmarshals the JSON payload:
func parseResource(classifyId int32, resourceMeta string) (res resource.DynamicResource, err error) {
tmp, has := classifyResourceMap[classifyId]
if !has { return nil, ecode.Error(ecode.NothingFound, "classifyResourceMap中不存在此classifyId") }
if len(resourceMeta) == 0 { return nil, ecode.Error(ecode.NothingFound, "resource字段为空") }
t := reflect.TypeOf(tmp).Elem()
ptr := reflect.New(t)
ptr.Elem().FieldByName("ClassifyId").SetInt(int64(classifyId))
res, ok := ptr.Interface().(resource.DynamicResource)
if !ok { return nil, ecode.Error(ecode.NothingFound, "classifyResourceMap reflect interface error") }
if err = json.Unmarshal([]byte(resourceMeta), res); err != nil { return nil, err }
return res, nil
}Applicability – DDD is suitable when business logic is complex, requires deep collaboration, and evolves continuously. It is less appropriate for mature, low‑complexity services or when the main challenges are pure performance concerns.
Challenges include team education, convincing management of ROI, and maintaining a shared domain language across groups.
Benefits listed are clearer layer separation, easier database migrations, reduced code duplication, and higher stability thanks to rich domain models.
Costs involve extra work for DTO/DO/PO mappings, the need for comprehensive design documentation, and the effort required to refactor existing codebases.
The article concludes with an outlook on further splitting domain services, scaling the architecture across the organization, and even using GPT to auto‑generate repetitive mapper code.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.