Upgrade to Pro — share decks privately, control downloads, hide ads and more …

まずはドメインに向き合って、それからCQRSで実装する

Avatar for memetics10 memetics10
January 10, 2026
1.1k

 まずはドメインに向き合って、それからCQRSで実装する

Avatar for memetics10

memetics10

January 10, 2026
Tweet

Transcript

  1. $234͸ΞʔΩςΫνϟύλʔϯͷҰछ͕ͩɺ ϓϩμΫτσβΠϯͷώϯτ͕຾͍ͬͯΔ ɾ.7$ ɾϨΠϠʔυΞʔΩςΫνϟ ɾΠϕϯτιʔγϯά ɾFUDʜ ɾϢʔβʔ΍υϝΠϯͷ෼ੳ ɾ֓೦Ϟσϧ ɾ69ઃܭ ɾFUDʜ

    ΞʔΩςΫνϟύλʔϯ ϓϩμΫτσβΠϯ #VU 8IZ$234 ιϑτ΢ΣΞͷ಺෦Λମܥతʹ੔ཧ͢Δํ๏ ϢʔβʔχʔζʹରԠ͢Δ੡඼Λ࡞੒͢Δϓϩηε
  2. ٕज़తʹ͸ͲͪΒ΋୯७ͳߏ଄ମͱ͍͏఺Ͱಉ͡Ͱɺ໋໊ํ๏ʹҧ͍͕͋Δ ݴޠతͳҙຯʹ஫໨͠ɺ υϝΠϯϞσϧͱϢϏΩλεݴޠͷ୳ࡧʹ࢖͏ type DeactivateInventoryItem struct { InventoryItemID string Comment

    string } type InventoryItemDeactivated struct { InventoryItemID string Comment string } &%"΋ɺΠϕϯτʹ࣍ͷࢦؚ͕ࣔ·Εͳ͍ҙຯతͳཧ༝Ͱૄ݁߹ʹͳΔ $PNNBOE&WFOU
  3. ΠϕϯτετʔϛϯάΛɺσβΠϯͷख๏ͱͯ͠׆༻͢Δ ҟͳΔόοΫάϥ΢ϯυΛ࣋ͭਓͷߟ͑Λൃࢄͤ͞ɺΧϥʔύζϧͱର࿩Λ௨ͯ͠߹ҙͤ͞Δ ·ͣ͸໰୊Λ޿͘୳ΓɺͦΕ͔Βղܾ΁ऩଋ͍ͤͯ͘͞఺ͰɺσβΠϯࢥߟʹΑ͘ࣅ͍ͯΔ Πϕϯτ ϙϦγʔ ू໿ ίϚϯυ ίϚϯυ Πϕϯτ ू໿

    %PO/PSNBO 5IF%FTJHOPG&WFSZEBZ5IJOHT ൴Β͸ɺຊ౰ͷ໰୊͕Կ͔ΛݟఆΊΔ·Ͱ͸ղܾΛ୳ͦ͏ͱ͠ͳ͍ɻ ໰୊͕ఆ·ͬͨͱ͖Ͱ͑͞ɺ໰୊Λղܾ͠Α͏ͱ͸͠ͳ͍ɻ ൴Β͸ཱͪࢭ·ͬͯ޿͍ൣғ͔ΒՄೳͳ౴͑Λ୳͢ɻͦ͏ͯ͠ॳΊͯɺ࠷ऴతʹ൴ΒͷఏҊΛऩଋ͍ͤͯ͘͞ɻ ͜ͷϓϩηε͸σβΠϯࢥߟͱݺ͹Ε͍ͯΔɻ &WFOU4UPSNJOH
  4. ஫จ͞Εͨ ஫จ͞Εͨ Βඞͣ ஫จ ஫จ͢Δ υϦϯΫΛ ࡞Δ υϦϯΫ υϦϯΫ͕ ࡞ΒΕͨ

    υϦϯΫ͕ ׬੒ͨ͠Β ඞͣ ٬ͷ໊લΛ ݺͿ ࢧ෷͍Λ͢ Δ ࢧ෷͍ ࢧ෷͍͕׬ ྃͨ͠ ࢧ෷͍Ͱ͖ ͳ͔ͬͨ৔ ߹͸ υϦϯΫΛ ࣺͯΔ ஫จ͢Δ ࢧ෷͍υ ϦϯΫ࡞੒ ࢧ෷͍υ ϦϯΫ࡞੒ ׬ྃ "HHSFHBUF1PMJDZ ੍໿ΛकΓͳ͕ΒɺॊೈͰϊϯϒϩοΩϯάͳΠϯλϥΫγϣϯΛߟ͑Δ (SFHPS)PIQF 4UBSCVDLT%PFT/PU6TF5XP1IBTF$PNNJU XFDBOTFFUIBUUIFSFBMXPSMEJTPGUFOBTZODISPOPVT 0VSEBJMZMJWFTDPOTJTUTPGNBOZDPPSEJOBUFE CVUBTZODISPOPVTJOUFSBDUJPOT %#ϩοΫ͢Ε͹͍͍ɺͱٕज़ओಋͰߟ͑ͳ͍Α͏ʹ͢Δ
  5. ঎඼ͷ஫จ͕ ׬ྃͨ͠ ஫จ ঎඼ͷ஫จΛ ׬ྃͤ͞Δ αϒεΫϦϓ γϣϯొ࿥͢ Δ αϒεΫ঎඼ ͷ৔߹

    αϒεΫϦϓ γϣϯొ࿥͞ Εͨ αϒεΫϦϓ γϣϯ ౤ߘ͕ ެ։͞Εͨ ౤ߘ ౤ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ؅ཧऀ ސ٬༻ͷ ౤ߘϏϡʔ 'JSTU.PEFM
  6. ঎඼ͷ஫จ͕ ׬ྃͨ͠ αϒεΫ঎඼ ͷ৔߹ ஫จ ঎඼ͷ஫จΛ ׬ྃͤ͞Δ αϒεΫϦ ϓγϣϯొ ࿥͢Δ

    αϒεΫϦϓ γϣϯొ࿥͞ Εͨ αϒεΫϦϓ γϣϯ ౤ߘ͕ ެ։͞Εͨ ౤ߘ ౤ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ؅ཧऀ ސ٬༻ͷ ౤ߘϏϡʔ αϒεΫϦϓ γϣϯొ࿥͞ ΕͨΒ ձһূΛड ͚औΔ ձһূ͕ड͚ औΒΕͨ ձһূ ձһূ ձһূ͕ఀࢭ ͞Εͨ ձһূΛఀ ࢭ͢Δ αϒεΫϦ ϓγϣϯΛ ແޮԽ͢Δ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞Εͨ αϒεΫϦϓ γϣϯ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞ΕͨΒ ސ٬ αʔϏε΁ͷࢧ෷͍ͱར༻ͷݖརΛ෼཭͠ɺ ձһূΛड͚औͬͨɺͱ͍͏ࣄ࣮Λ௥Ճ 3FDPOTJEFS%PNBJO.PEFM
  7. Collaborative Domain Independent Domain 1VCMJD%BUB ঎඼Ձ֨ 1SJWBUF%BUB ঎඼໊ ঎඼આ໌ ঎඼ը૾

    CRUD, Stereotype Architecture CQRS+ES Publish Data 6EJ%BIBO IUUQTYDPN6EJ%BIBOTUBUVT 1VCMJDEBUBXPVMEOPUCFIBOEMFEJOUIFTBNFWFSUJDBMTMJDFBTUIFQSJWBUFEBUB $234JTBNPSFBQQSPQSJBUFDIPJDFGPSQVCMJDEBUB BOEUIFSF  JUDBONBLFTFOTFUPIBWFEJ ff FSFOUWFSUJDBMTMJDFTGPSUIFDPNNBOEBOERVFSZQBSUT #PVOEFE$POUFYU 5PQ-FWFM"SDIJUFDUVSF
  8. package subscription // ू໿ϧʔτ type Subscription struct { ID uuid.UUID

    Status Status } // ू໿ϧʔτͷίϚϯυϝιου͕ɺෆม৚݅Λҡ࣋͠ͳ͕ΒΠϕϯτΛ࡞੒͢Δ func (s Subscription) Deactivate(deactivateAt time.Time) (Deactivated, error) { // write some invariants here if s.Status == Deactivated { return Deactivated{}, fmt.Errorf("subscription is already deactivated: %w", errors.ErrPreconditionFailed) } return Deactivated{SubscriptionID: s.ID, DeactivateAt: deactivateAt}, nil } // Πϕϯτ type Deactivated struct { SubscriptionID uuid.UUID DeactivatedAt time.Time } func (SubscriptionDeactivated) IsSubscriptionEvent() {} type Event interface { IsSubscriptionEvent() } // ϦϙδτϦ: Πϕϯτͷอଘͱू໿ͷϩʔυ͕Մೳ type Repository interface { Store(context.Context, Event) error Load(context.Context, uuid.UUID) (Subscription, error) } *NQMFNFOUJOH"HHSFHBUF
  9. package command // ίϚϯυ: ૢ࡞ͷ໊લͱͦΕ࣮ߦ͢ΔͨΊʹඞཁͳೖྗσʔλΛ࣋ͭ type DeactivateSubscriptionCommand struct { SubscriptionID

    uuid.UUID } // ίϚϯυϓϩηοαʔ: ू໿ʹίϚϯυॲཧΛҕৡ͢Δ func (p DeactivateSubscriptionCommandProcessor) Exec(ctx context.Context, command DeactivateSubscriptionCommand) (subscription.Deactivated, error) { aSubscription, err := p.SubscriptionRepo.Load(ctx, command.SubscriptionID) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Load: %w", err) } event, err := aSubscription.Deactivate(time.Now()) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Subscription.Deactivate: %w", err) } if err := p.SubscriptionRepo.Store(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Store: %w", err) } if err := p.EventPublisher.Publish(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("EventPublisher.Publish: %w", err) } return event, nil } *NQMFNFOUJOH$PNNBOE
  10. package redis type SubscriptionDeactivatedEventHandler struct { AsynqClient *asynq.Client } func

    (h SubscriptionDeactivatedEventHandler) HandleSubscriptionDeactivated(ctx context.Context, event subscription.Deactivated) error { // Ϗδωεϧʔϧ͸੾Γग़͓ͯ͘͠ if policy.CanDeactivateMembershipcard(event) { payload, err := json.Marshal(task.DeactivateMembershipcardPayload{SubscriptionID: event.SubscriptionID}) if err != nil { return fmt.Errorf("marshal payload: %w", err) } // Ωϡʔʹ٧ΊͯผϓϩηεʹॲཧΛ೚ͤΔ _, err = h.AsynqClient.EnqueueContext(ctx, asynq.NewTask(task.DeactivateMembershipcard, payload)) if err != nil { return fmt.Errorf("enqueue process deactivate membershipcard task: %w", err) } } return nil } ˞࣮ߦͰ͖ͳ͍৔߹ͷϦτϥΠՄೳੑɺෳ਺ճ࣮ߦ͞Εͨ৔߹ͷႈ౳ੑɺଞͷΠϕϯτͱͷॱংੑɺΛߟྀ͓ͯ͘͠ඞཁ͋Γ *NQMFNFOUJOH1PMJDZ
  11. package query // ΫΤϦαʔϏε type SubscriptionService interface { QueryPage(ctx context.Context,

    filter SubscriptionFilter, sorter SubscriptionSorter, limit int, cursor *string) (Page[Subscription], error) QueryByIDs(context.Context, []string) ([]Subscription, error) QueryByID(context.Context, string) (Subscription, error) } // ΫΤϦϞσϧ type Subscription struct { ID string CustomerID string ProductID string NextRenewAt time.Time LastRenewedAt time.Time } // ϑΟϧλʔ&ιʔτ type SubscriptionFilter struct { CustomerID string } type SubscriptionSorter struct { Key SubscriptionSortKey Direction SortDirection } 2VFSZ4FSWJDF.PEFM