Slide 1

Slide 1 text

Case of Ad Delivery System is Implemented by Scala and DDD 阿川 耕司 (AGAWA Koji) AdTech Studio / CyberAgent, Inc.

Slide 2

Slide 2 text

AGAWA Koji •Software engineer at CyberAgent, Inc. Work on Scala for 4 years Work on DDD for a year 阿川 耕司 atty303 @atty303

Slide 3

Slide 3 text

1.What’s Ad Delivery System? 2.Our Domain Driven Design 3.Architecture 4.Domain Layer 5.Summary

Slide 4

Slide 4 text

What’s Ad Delivery System? 広告配信システムとは?

Slide 5

Slide 5 text

Ecosystem of online advertisement ネット広告はこのような構造になっています

Slide 6

Slide 6 text

Our system is DSP 今回お話するシステムはこの中のDSPになります

Slide 7

Slide 7 text

DSP is system for RTB DSPはRTB(Real Time Bidding)のシステムです https://blog.adcash.com/what-is-rtb-and-why-it-is-important/

Slide 8

Slide 8 text

Programmatic Ad Buying as a Service GlazeというDSPを作っています Our DSP is ...

Slide 9

Slide 9 text

Why Glaze? •Delivering ads with RTB has become generalized •Things other than RTB, such as creativity, become important •Glaze provide basic functions such as bidding, delivery and measurement required by RTB as SaaS •Glaze goal is to be able to focus on the intrinsic strengths of the customer's products なぜGlazeを作っているのかは本題ではないので簡単に説明し ます

Slide 10

Slide 10 text

Why have we chosen DDD? • Probably 8th times (!) to make DSP with Adtech Studio ... • Out motivation is to create something that can withstand long-term maintenance • We have decided to introduce DDD as a proper design method 長期的に持続できるシステムを作るためにDDDを選択しました

Slide 11

Slide 11 text

Our Domain Driven Design DDDによる設計

Slide 12

Slide 12 text

History of development • Focused on domain modeling early in development • Coded only the domain layer • Working with a small team • We discussed the domain model deeply Domain modeling (2~3 persons) Mob programming (4~5 persons) 開発の初期は小さなチームでドメインモデリングに集中しまし た 2018/05 2018/08 2018/10 Development continues… (4~8 persons)

Slide 13

Slide 13 text

History of development • After the first domain modeling was done, we proceeded development with mob programming • Coding until one function works completely • Working with a middle team • We proceeded with agreement on architecture and coding conventions Domain modeling (2~3 persons) Mob programming (4~5 persons) ドメインモデリングが終わった後はモブプログラミングでチーム 内の意識を合わせながら開発を進めました 2018/05 2018/08 2018/10 Development continues… (4~8 persons)

Slide 14

Slide 14 text

History of development • The first release on 2018/12 • We continue to develop with Agile Domain modeling (2~3 persons) Mob programming (4~5 persons) その後もアジャイルで開発を継続しています 2018/05 2018/08 2018/10 Development continues… (4~8 persons)

Slide 15

Slide 15 text

Domain Driven Design DDDの構成要素です https://github.com/ketan-gote/ddd-example

Slide 16

Slide 16 text

Context map まずはコンテキストマップについてお話します

Slide 17

Slide 17 text

Context map progadが主となるBCで、コアドメインでもあります programmatic ad buying (same as core domain)

Slide 18

Slide 18 text

Context map 入札ロジックは独立したBCが責任を持ちます bidding logic (pricing)

Slide 19

Slide 19 text

Context map 入札ロジックを別チームであるGlazeのパートナーがカスタマイ ズできるようになっています Partner can customize bidding logic

Slide 20

Slide 20 text

Context map progadとbid agentが共有するドメインオブジェクトをkernelとして います the kernel shared by other contexts

Slide 21

Slide 21 text

Bounded context BCの設計にはコンウェイの法則、逆コンウェイ戦略を意識しま した • Consciously aligned with team boundaries • Conway’s Law & Inverse Conway Maneuver • Bounded context ≒ Microservice boundary • If you are wondering if you should split BC, I think it is better to proceed without splitting Make one team responsible for one BC

Slide 22

Slide 22 text

Domain Driven Design 次はエンティティと値オブジェクトについてです https://github.com/ketan-gote/ddd-example

Slide 23

Slide 23 text

11 Entities ドメインモデルには11のエンティティがあります AppList Ad Conversion LineItem Exchange AudienceIdentifierList BudgetAccount Advertiser InsertionOrder CompleteCandidate Campaign

Slide 24

Slide 24 text

363 Value objects 値オブジェクトは363あります NoBidReasonCode VideoCreativeFile DeliveryEvent ContentTaxonomy AdCompanions BidAgentConfig WinNotice AggregateSpending Extension ApiFramework Sketch ClickDeliveryTracking Content Endpoint EntityLikeDescriptorSet OperatingSystem Tracker Markup AcceptanceRule Currency Purpose Adjust FeatureEngineer Feature UnboundAssets VideoFileCreative DeliveryUnboundEndpoints DistributionChannel AggregationGroup CostPerView FilterResult BidResponseTranslator DisplayAdPosition WinDeliveryTracking FeatureIndexer AttributionId VastExtension Dimension VideoCandidateFilter PartialCandidate Descriptor BitSetDescriptorSet Tracking DeliveryBoundEndpoints ViewMeasurementTracking BlankProto FeatureSource package HtmlMarkup IosAdvertisingIdentifier AuctionNotice ConversionRate CandidateHeader Cpm ViewableImpressionTrackingType Imp BidAgentConfigured BaseMetric AdType ListOwner VideoAdMeasurementContext CreativesDigest Candidate MeasurementUrls AudioCreative BidContext CompanionType Geo DisplayViewableDefinition Publisher PageUrl Metric Source Tune VideoAdFilter NegativeTargeting AcceptedActivity ListIntent ImageCreativeFile MeasurementEventProto AdtdpComCookieIdentifier DeniedActivity InventoryDescriptors ActivitySet CandidateRejection WebTargeting TargetingCategory ClickThroughRate MobileAppId ClickMeasurementTracking OperatingSystemDescriptor Targeting CPVBidAgentFeature MeasurementTracking TrackerBoundEndpoint LinearityMode Restrictions InappropriateContentDescriptor BlankPayload CandidateFilter AudioPlacement Frequency AudienceIdentifierListMeta Ipv4 RejectableCandidate Fox IosAppId CostPerAcquisition AndThenAdFilter NoBidReasonClass StandardOrderPath UserRewardedContentDescriptor CustomerAction SystemListOwner CustomerActionProto TimingProfile DescribableAsEngine MraidInfo AndroidAdvertisingIdentifier CostRate AndroidAppId DimensionUnit ClickTracker Format DisplayCreative RejectedAd EventType Ip MeasurementEvent BidRequest NamedCustomerAction HourFeature MeasurementState RealtimeBiddingProto Origin Candidates DeviceCategory Credit Site AdPreviewEndpoint AtomicFeature AttachmentPoint InventorySource VideoContainer TrackingSignal AdFilter KeyPerformanceIndicator AppsFlyer CandidateEvaluation MobilePlatformSpecificIdentifier EnvironmentTargeting CookieIdentifier BuiltinMeasurement DelayedFeedback Queryable DeliveryEventEncoder UserRewardedContentTargeting DisplayCreativeSubtype SeparatedCandidates MoneyExchange Ip DeliveryContext Viewability NoBidReasonCategory DeliveryState EnvironmentDescriptor MeasurementTool InteractionOfFeatures RichCreative ImpressionDeliveryTracking AdTypeDescriptor OrderPath VideoCreative package OpenRTBContentTaxonomy Bitrate OrderStatus User DescriptorSet ImageFormat package Budget ConnectionType AdvertiserListOwner Currency CookieDomain ImageCreative CreativeType Publisher Banner MarkupService BidPayload MeasurementEventEncoder ChargeModel MetricUnit UserAgentParser AudienceStats Ad AggregatePeriod SensitiveCategoryTargeting Audience GlazeTestBidRequestTranslator RtbShipState SpendingCapTarget VideoPayload Attribute StandardBidRequest Bid WinPayload FixedPricingBidAgentConfig TrackingContext AppTargeting CapStatus StandardBidAgentConfig DeviceProto ValueLikeDescriptorSet CardinalityDefinable AudioVideoCreativeSubtype DeliveryTracking CPVBidAgentConfig FeatureValueReader VideoFile MopubExtension ImpressionTracker ImpressionUnitPrice Indexable Environment ClickProto TargetingCompound VerifiedActivity CommonnessResolvable LastClickAttributionModel StandardBidAgentFeature AppListEntry DomainDescriptor Purpose MobilePlatformTargeting Video Money CustomerActionPayload Translatable BidDeliveryTracking ExchangeRepository AbstractEntityBasedTargeting Clause AppListMeta ViewabilityDescriptor StartDelayMode WinProto TradingLocation AudienceProto BidRequest AdSelector WinTracker VideoFiles CallToAction AppDescriptor VideoPlacement ExchangeProtocol AggregatedFrequency TemplateUrl AndThenCandidateFilter VideoProto Data BidRequestTranslator CreativeMeta ConversionEvent MopubProtocol Identifier CapTarget ViewThroughRate MopubVastExtension MopubBidRequestTranslator LoseNotice AuditStatus AdsDigest AttributionModel Money BinaryClassifier GoogleAdManagerProtocol ViewableDefinition CandidateContext FeatureIndexerTransformer ConversionClassifier Features NoExtension Producer VideoViewableDefinition SeparatedAds App StandardBidResponse InappropriateContentCategory Vast3Markup MeasurementUrl SeatBid Regs VideoCreativesDigest Activity GUIDPool Playhead Creative DurationRange TrackerUnboundEndpoint StandardContentTaxonomy AudienceDescriptor Cpm CoproductUrl BoundAssets Mraid2VastMarkup Audience MopubBidResponseTranslator RedirectToLandingPage VastTrackingEvent SpecReadable CreativeAttribute LogisticRegression ClickPayload GlazeTestBidResponseTranslator MediaType Geo BudgetAccountStatus VideoTrackerBoundEndpoint Device GoogleWinningPriceCodec EndpointResolver Ipv6 InvalidTrafficClassification CreativeStatus CustomerActionMeasurementTracking ExchangeDescriptor AudienceTargeting BidResponse ParsedUserAgent NoBidReason DeviceType DeliveryEventProto Ipv6 AdFormat SimpleTrackerBoundEndpoint VideoDeliveryTracking ContentCategoryDescriptor EventForAggregate AttributeValue CampaignPurpose Segment Policy VideoTracker ListOwnerType BidAgent AudienceIdentifierListEntry BidProto Placement package BidResponse Ipv4 WinningPriceCodec Device TrackingUrl LastTouchAttributionModel FactorizationMachine FrequencyCap AbstractValueBasedTargeting VideoProfile AssetPath DisplayPlacement package VideoCreativeMeta ExtensionTrackingType Spent AdPosition MoneyProto NaiveDescriptorSet LandingPage Attribution MarkupRequest Audit Item CustomerActionActivation DerivedMetric

Slide 25

Slide 25 text

Entity & Value object design • At first I tended to make Entity • Anyway, let's create a Value Object ! • Do not use primitive types for members 最初はエンティティを作りがちですが、可能な限り値オブジェク トに出来ないか考えましょう

Slide 26

Slide 26 text

Architecture

Slide 27

Slide 27 text

Architectures アーキテクチャには色々な選択肢がありました

Slide 28

Slide 28 text

Explicit architecture 特にこれといったものは採用せず、Explicit Architecture という 記事を参考に設計を進めました https://herbertograca.com/tag/explicit-architecture/

Slide 29

Slide 29 text

Most important thing is... ただ一つ、「ドメインを技術的詳細から隔離する」ことが重要で す Isolate the domain from technical details

Slide 30

Slide 30 text

...but, our domain is Programmatic Ad Buying, some technical details placed to domain. でも私たちのドメインはネット広告なので、一般的には技術的 詳細に分類させるものもドメインに置いています

Slide 31

Slide 31 text

Some technical details? • Connection protocol with other Exchange • Measurement URL by pixel 例えばExchangeとの接続プロトコルや計測URLをドメインに置い ています

Slide 32

Slide 32 text

27 SBT projects まずはSBTのプロジェクト構成です

Slide 33

Slide 33 text

Simplified project structure 簡略化するとこのようになります

Slide 34

Slide 34 text

Project dependency • Represent one-way dependency of layer by project division • Force not to write wrong dependency from outside to inside layer レイヤーの外から内方向への間違った依存を書けないように プロジェクト分割して強制しました

Slide 35

Slide 35 text

Foundation すべてのファンデーションプロジェクト •So-called common library •Axis different from layer division •Stores the base code of domain, application and infrastructure layer •Very small (about 1 to 10 classes)

Slide 36

Slide 36 text

Domain / Application layer すべてのドメインとアプリケーションプロジェクト •Directory is divided by Bounded Context •I think I should separate repositories by BC correctly •Spark applications are split because they have different paradigms •The project is not divided into applications because it is still small

Slide 37

Slide 37 text

Application layer • Separate packages by functional module • Classify for each use case アプリケーションは機能モジュールごとのパッケージ、ユース ケースごとのクラスになっています

Slide 38

Slide 38 text

Use case class • Dependency injection by constructor injection at infrastructure layer • Nothing specials ユースケースクラスはコンストラクタDIにより構成されています trait HandleClickUseCase { def apply(exchangeIdStr: String, uri: String): Future[Url] } class HandleClickUseCaseImpl( trackerEndpointResolver: TrackerEndpointResolver.CustomResponse, handleEndpointSignalUseCase: HandleEndpointSignalUseCase, adRepository: AdRepository, transactor: Resource[IO, Transactor[IO]] ) extends HandleClickUseCase with StrictLogging { }

Slide 39

Slide 39 text

Adapter (Infrastructure layer) すべてのアダプタプロジェクト

Slide 40

Slide 40 text

Adapter example Trait is placed in application layer package glaze.progad.application.budget.repository trait SpendingRepository { def append(budgetAccountId: BudgetAccountId, events: Seq[Spent]): Future[Unit] def sum(budgetAccountId: BudgetAccountId, period: AggregatePeriod): Future[Seq[AggregateSpending]] } package glaze.progad.adapter.repository.dynamodb.spending class DynamodbSpendingRepository( client: AmazonDynamoDBAsync @@ DynamodbSpendingRepository, tableName: String @@ DynamodbSpendingRepository, scanamoEc: ExecutionContext @@ DynamodbSpendingRepository ) extends SpendingRepository { override def append(budgetAccountId: BudgetAccountId, events: Seq[Spent]): Future[Unit] = ??? } リポジトリの trait はアプリケーション層に配置しています Implement it as concrete class with constructor dependency injection

Slide 41

Slide 41 text

Repository trait in application layer • Can force the domain to be pure (no Future, no IO) • In the DDD example, repository traits are often placed in the domain layer それによりドメイン層が純粋であることを強制することができま す

Slide 42

Slide 42 text

Driver (Infrastructure layer) すべてのドライバプロジェクト • The dependency on the HTTP framework is only at this layer • DI works in this layer • Other projects besides Scala are included (frontend, Lambda function)

Slide 43

Slide 43 text

Domain Layer

Slide 44

Slide 44 text

The domain layer is a chunk of domain-specific work, so I will talk about parts that can be used generically. ドメイン層はドメイン固有業務の塊なので、汎用的に使える部 分をお話します

Slide 45

Slide 45 text

Codec type class package glaze.foundation.core import com.twitter.util.{Decoder, Encoder} trait Codec[T, S] extends Encoder[T, S] with Decoder[Option[T], S] object Codec { def encode[T, S](t: T)(implicit enc: Codec[T, S]): S = enc.encode(t) def decode[T, S](s: S)(implicit dec: Codec[T, S]): Option[T] = dec.decode(s) def apply[T, S]()(implicit codec: Codec[T, S]): Codec[T, S] = codec def apply[T, S](encoder: T => S)(decoder: S => Option[T]): Codec[T, S] = new Codec[T, S] { override def encode(t: T): S = encoder(t) override def decode(s: S): Option[T] = decoder(s) } } Codecというシリアライズをサポートする型クラスです

Slide 46

Slide 46 text

Example of Codec package glaze.kernel.domain import enumeratum.values.{StringEnum, StringEnumEntry} import glaze.foundation.core.Codec sealed abstract class AdType(val value: String) extends StringEnumEntry object AdType extends StringEnum[AdType] { val values: immutable.IndexedSeq[AdType] = findValues object Display extends AdType("Display") object Video extends AdType("Video") object Audio extends AdType("Audio") implicit val codecForAdTypeString: Codec[AdType, String] = Codec((_: AdType).value)(s => AdType.withValueOpt(s)) } 型クラスインスタンスはCodecはこのように実装します

Slide 47

Slide 47 text

Usage of Codec Codec.encode[AdType, String](AdType.Video) → “Video” Codec.decode[AdType, String]("Video") → Some(AdType.Video) Codec.decode[AdType, String]("Invalid") → None 型クラスはこのように利用します

Slide 48

Slide 48 text

Codec to Doobie adapter package glaze.foundation.application.doobie import doobie.util.Meta import glaze.foundation.core.Codec trait Implicits { implicit def codecToDoobieMeta[T, S](implicit codec: Codec[T, S], ev: Meta[S]): Meta[T] = Meta[S].imap(r => codec.decode(r).getOrElse(throw new IllegalArgumentException(s"invalid format: $r")))(codec.encode) } Codecを用意するとこの定義によりDoobieのMeta型クラスへ暗 黙変換されます If you prepare Codec, this definition will implicitly convert to Doobie's Meta type class

Slide 49

Slide 49 text

Codec to Scanamo adapter package glaze.foundation.aws.dynamodb import glaze.foundation.core.Codec import org.scanamo.DynamoFormat import org.scanamo.error.TypeCoercionError trait Implicits { implicit def codecToDynamoFormat[T, S](implicit codec: Codec[T, S], f: DynamoFormat[S]): DynamoFormat[T] = DynamoFormat.xmap[T, S] { r => codec.decode(r).toRight(TypeCoercionError(new IllegalArgumentException(s"invalid format: $r"))) } { codec.encode } } ScanamoのDynamoFormat型クラスへの変換も同時に利用可 能になります Conversion to Scanamo's DynamoFormat type class will also be available at the same time

Slide 50

Slide 50 text

Should be Codec in domain layer? • I wondered if this transformation would be good for the domain layer この変換をドメイン層に置いて良いものか迷いました

Slide 51

Slide 51 text

Should be Codec in domain layer? • I decided to put in the domain layer only about things that can be converted to primitives • Codec is not defined for an entity and a value object consisting of multiple value objects • It is expected that primitive representations will not change even if complex representations such as JSON are shaken in the future プリミティブと変換できるものについてのみドメイン層に置くこと にしました

Slide 52

Slide 52 text

Should Codec be in domain layer? • Reduced conversion code between layers, so development was speeding up • There is no need to write separate conversions with Doobie (RDB) and Scanamo (KVS) これにより大きなデメリットもなく開発スピードが向上したと思い ます

Slide 53

Slide 53 text

Domain event Domain events are defined in the domain layer by Protocol Buffers もう一つ、ドメインイベントの例です

Slide 54

Slide 54 text

Why is .proto in domain layer? • At first, .proto was placed in the application layer, and in the domain layer the corresponding case class was written • Reduced development speed with conversion code hell • Moved .proto to the domain layer and used ScalaPB generated case class as domain event 変換コードのメンテナンスが非常に辛かったのでドメイン層に 置くことにしました

Slide 55

Slide 55 text

Why .proto in domain layer? • Code generation from schemas such as Protocol Buffers and Avro is not fit with DDD • The advantage of code autogeneration disappears when isolated from domain • There are also cases where the generated code is used in the domain layer • I think that the problem is unlikely to occur because Protocol Buffers is schema compatible at the time of modification 割り切って自動生成コードをドメイン層で利用するのもありだと 思います

Slide 56

Slide 56 text

Summary

Slide 57

Slide 57 text

Thank you ありがとうございました Special thanks to @bakenezumi, @fukuo33