Case of Ad Delivery System is Implemented by Scala and DDD

Case of Ad Delivery System is Implemented by Scala and DDD

ScalaMatsuri 2019

De2cd22cd6242773153ee76de1c9ecdb?s=128

AGAWA Koji

June 28, 2019
Tweet

Transcript

  1. 1.

    Case of Ad Delivery System is Implemented by Scala and

    DDD 阿川 耕司 (AGAWA Koji) AdTech Studio / CyberAgent, Inc.
  2. 2.

    AGAWA Koji •Software engineer at CyberAgent, Inc. Work on Scala

    for 4 years Work on DDD for a year 阿川 耕司 atty303 @atty303
  3. 9.

    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を作っているのかは本題ではないので簡単に説明し ます
  4. 10.

    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を選択しました
  5. 12.

    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)
  6. 13.

    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)
  7. 14.

    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)
  8. 21.

    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
  9. 24.

    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
  10. 25.

    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 最初はエンティティを作りがちですが、可能な限り値オブジェク トに出来ないか考えましょう
  11. 30.

    ...but, our domain is Programmatic Ad Buying, some technical details

    placed to domain. でも私たちのドメインはネット広告なので、一般的には技術的 詳細に分類させるものもドメインに置いています
  12. 31.

    Some technical details? • Connection protocol with other Exchange •

    Measurement URL by pixel 例えばExchangeとの接続プロトコルや計測URLをドメインに置い ています
  13. 34.

    Project dependency • Represent one-way dependency of layer by project

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

    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)
  15. 36.

    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
  16. 37.

    Application layer • Separate packages by functional module • Classify

    for each use case アプリケーションは機能モジュールごとのパッケージ、ユース ケースごとのクラスになっています
  17. 38.

    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 { }
  18. 40.

    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
  19. 41.

    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 それによりドメイン層が純粋であることを強制することができま す
  20. 42.

    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)
  21. 44.

    The domain layer is a chunk of domain-specific work, so

    I will talk about parts that can be used generically. ドメイン層はドメイン固有業務の塊なので、汎用的に使える部 分をお話します
  22. 45.

    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というシリアライズをサポートする型クラスです
  23. 46.

    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はこのように実装します
  24. 47.

    Usage of Codec Codec.encode[AdType, String](AdType.Video) → “Video” Codec.decode[AdType, String]("Video") →

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

    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
  26. 49.

    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
  27. 50.

    Should be Codec in domain layer? • I wondered if

    this transformation would be good for the domain layer この変換をドメイン層に置いて良いものか迷いました
  28. 51.

    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 プリミティブと変換できるものについてのみドメイン層に置くこと にしました
  29. 52.

    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) これにより大きなデメリットもなく開発スピードが向上したと思い ます
  30. 53.

    Domain event Domain events are defined in the domain layer

    by Protocol Buffers もう一つ、ドメインイベントの例です
  31. 54.

    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 変換コードのメンテナンスが非常に辛かったのでドメイン層に 置くことにしました
  32. 55.

    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 割り切って自動生成コードをドメイン層で利用するのもありだと 思います
  33. 56.